superuser404notfound/AetherEngine
GitHub: superuser404notfound/AetherEngine
一个专为iOS、tvOS和macOS设计的视频播放器引擎,简化复杂视频播放后端的开发。
Stars: 22 | Forks: 4
AetherEngine
一个适用于Apple平台的视频播放器引擎。
FFmpeg解复用。VideoToolbox解码。AVPlayer处理Dolby Atmos。
您只需提供UI。
## 这是什么
一个能完美处理核心难点(HDR、Dolby Vision、Dolby Atmos、容器支持、编解码器支持)的播放器引擎,它仅暴露一个 `AetherPlayerView` (用于UIKit/AppKit) 或 `AetherPlayerSurface` (用于SwiftUI) 以及少量的 `async` 方法。没有 `AVPlayerViewController`。没有固化的控件。没有分析功能。只需绑定视图,调用 `play()`,通过已发布的属性读取状态即可。
视图是多态的:在底层,引擎会根据会话情况替换托管的CALayer(原生AVPlayer路径使用 `AVPlayerLayer`,软件dav1d回退路径使用 `AVSampleBufferDisplayLayer`),而宿主应用无需感知。
由您提供播放进度条。由您提供下拉菜单。由您提供美观的界面。
## 它能处理什么
| 领域 | 细节 |
| ----------- | --------------------------------------------------------------------------------------------------------------------------- |
| 容器 | MKV, MP4, WebM, MPEG-TS, AVI, OGG, FLV (解复用侧) |
| 硬件解码 | H.264, HEVC, HEVC Main10 通过 VideoToolbox 在 AVPlayer 的 HLS-fMP4 路径中实现。在支持硬件 AV1 的设备 (M3+ Mac, iPhone 15 Pro+, 未来的Apple TV芯片) 上,AV1 也同样原生路由 |
| 软件解码 | AV1 (libavcodec/dav1d) 在不支持硬件AV1的设备上——目前包括所有Apple TV、M1/M2 Mac、A17 Pro之前的iPhone。VP9 (libavcodec原生) 无条件支持,因为AVPlayer的HLS流水线会拒绝 `vp09` CODECS属性,即使VideoToolbox能硬件解码它。两者都通过 `SoftwareVideoDecoder` + `AVSampleBufferDisplayLayer` 进行渲染。调度决策位于 `AetherEngine.load` 中,并根据源通过 `VTCapabilityProbe` 控制 |
| HDR10 | BT.2020 + PQ 通过HLS-fMP4包装器发出信号;AVPlayer将比特流交给系统HDR处理流水线 |
| HDR10+ | 逐帧ST 2094-40动态元数据在流复制到HLS-fMP4包装器的过程中被保留 |
| Dolby Vision| HEVC P5 / P8.1 / P8.4 配合 `dvh1` / `hvc1` 轨道类型 + `dvcC` 框。AV1 P10.0 / P10.1 / P10.4 配合 `dav1` / `av01` 轨道类型 + `dvvC` 框(遵循Apple HLS Authoring Spec + Dolby ETSI TS 103 572)。两者在支持DV的显示器上都能激活tvOS的HDMI DV握手 |
| HLG | 检测到传输函数并发出信号 |
| HDR 到 SDR | 由AVPlayer / 系统合成器根据连接的显示器处理;主机端无色调映射 |
| 音频 | AAC, AC3, EAC3, FLAC, MP3, Opus, Vorbis, TrueHD, DTS, DTS-HD MA, ALAC, PCM |
| Dolby Atmos | EAC3+JOC 流通过HLS-fMP4包装器进行流复制,由AVPlayer播放,并在下游通过Dolby MAT 2.0解包 |
| 环绕声 | 5.1 / 7.1 配合正确的 `AudioChannelLayout` 在包装器中保留 |
| 字幕 | SubRip / ASS / SSA / WebVTT / mov_text 流式内联;PGS / HDMV PGS / DVB / DVD 渲染为 `CGImage` 并具有标准化位置;独立的 `.srt` / `.ass` / `.vtt` URL 通过短期上下文解码 |
| 跳转 | 向后/大跨度向前跳转时,进行生产者拆卸 + 重启;小范围向前跳转利用缓存的段窗口 |
| 流媒体 | 通过 `URLSession` 的HTTP Range + 分块委托读取 |
| 容错性 | 瞬时网络错误时进行指数退避,后台暂停,显示链接感知的生命周期管理 |
## 快速开始
```
import AetherEngine
import SwiftUI
let player = try AetherEngine()
// SwiftUI: drop AetherPlayerSurface anywhere in the view tree
var body: some View {
AetherPlayerSurface(engine: player)
}
// UIKit / AppKit: bind an AetherPlayerView directly
let surface = AetherPlayerView()
player.bind(view: surface)
try await player.load(url: videoURL) // or
try await player.load(url: videoURL, startPosition: 347.5) // resume
try await player.load(
url: videoURL,
options: .init(
httpHeaders: headers, // attached to every demux + segment fetch
matchContentEnabled: matchContent // tvOS Match Content master toggle
)
)
try await player.reloadAtCurrentPosition() // background reopen, preserves options
player.play()
player.pause()
player.setRate(1.5)
await player.seek(to: 120)
player.stop()
// Observe (Combine @Published)
player.$state // .idle, .loading, .playing, .paused, .seeking, .error
player.$currentTime // AVPlayer's HLS clock (use for transport / scrub / resume)
player.$sourceTime // source PTS of the displayed frame (use for subtitle alignment)
player.$duration
player.$videoFormat // .sdr, .hdr10, .hdr10Plus, .dolbyVision, .hlg
player.$currentAVPlayer // active AVPlayer, re-emitted on every reload (MPNowPlayingSession)
player.audioTracks // [TrackInfo]
player.selectAudioTrack(index: trackID)
// tvOS info panel / Now Playing
player.setExternalMetadata([
AVMetadataItem(/* title, artwork, etc. */)
])
// Subtitles, text and bitmap, one published list
player.subtitleTracks // [TrackInfo] for the loaded source
player.selectSubtitleTrack(index: streamID) // embedded, text or bitmap
player.selectSidecarSubtitle(url: srtURL) // .srt / .ass / .vtt next to the media
player.clearSubtitle()
player.$subtitleCues // [SubtitleCue], body is .text(String) or .image(SubtitleImage)
player.$isSubtitleActive // host mirror gate
player.$isLoadingSubtitles // sidecar fetch + decode in progress
```
字幕提示位于原始源PTS。在原生路径上,AVPlayer的HLS时钟位于 `source_pts - producer.videoShiftPts`(生产者应用一个每会话的偏移量,以将第一个片段的tfdt与播放列表原点对齐,并且该偏移量在每次重启时都可能变化)。请针对 `player.sourceTime` 渲染覆盖层,这样无论哪个生产者会话处于活动状态,字幕提示都能与口语音频匹配。
通过Swift Package Manager安装:
```
.package(url: "https://github.com/superuser404notfound/AetherEngine", branch: "main")
```
## 播放流水线
AetherEngine有两个播放流水线,在 `load(url:)` 时根据源的视频编解码器一次性选择:
**原生AVPlayer流水线(默认)。** 使用libavformat解复用源,实时将基本流重新复用为HLS-fMP4,通过 `127.0.0.1:
` 上的本地HTTP服务器提供服务,并将 `AVPlayer` 指向播放列表。Apple的软件栈负责所有解码、所有通过HDMI的HDR / Dolby Vision信号传输、所有音频路由。这是用于HEVC和H.264的路径,因为AVPlayer的HLS-fMP4流水线可靠地接受它们。Atmos直通、DV HDMI握手、HDR10 / HDR10+ 系统端色调映射都位于此路径上。
```
Source URL ──► Demuxer ──► HLSSegmentProducer ──► SegmentCache ──► HLSLocalServer
│
▼
AVPlayer
│
├─► VideoToolbox (HW decode)
└─► AVR / speakers (Atmos via MAT 2.0)
```
**软件解码器流水线 (AV1 + VP9回退)。** 解复用源,将视频数据包通过libavcodec(AV1使用dav1d,VP9使用FFmpeg的原生VP9解码器)处理为 `CVPixelBuffer`,将音频通过libavcodec处理为 `CMSampleBuffer`,使用 `AVSampleBufferDisplayLayer` + `AVSampleBufferAudioRenderer` 并配合 `AVSampleBufferRenderSynchronizer` 作为主时钟进行渲染。用于AVPlayer的HLS-fMP4流水线不接受的编解码器:AV1(在tvOS上完全没有AV1解码器;Apple仅在iOS / macOS上提供dav1d,没有Apple TV芯片支持硬件AV1)和VP9(AVPlayer解析HLS清单,看到CODECS属性中的 `vp09`,然后静默停止获取——`item.status` 永远不会离开 `.unknown`。VideoToolbox可以硬件解码VP9,但仅在HLS流水线之外)。
```
Source URL ──► Demuxer ──┬─► SoftwareVideoDecoder (dav1d) ──► SampleBufferRenderer
│ │
│ ▼
│ AVSampleBufferDisplayLayer
│ ▲
└─► AudioDecoder ──► AudioOutput ────────────┘
│ (synchronizer drives the layer's
▼ control timebase → A/V sync)
AVR / speakers
```
AV1+DV(Profile 10.0 / 10.1 / 10.4)在支持硬件AV1的主机上通过 `dav1` / `av01` 轨道类型加上源的 `dvvC` 框经由原生路径路由。AV1+Atmos在现实中极其罕见(母版制作仍绝大多数运行在HEVC上),因此软件流水线缺乏Atmos直通是一个理论限制而非实际限制。调度在加载时发生一次;无论哪种情况,宿主应用看到的都是统一的 `@Published` 状态接口。
为什么原生路径使用HLS-fMP4而不是直接向 `AVPlayer` 提供源URL:AVPlayer的渐进式下载路径不接受任意的MKV容器,即使是MP4源,在Dolby Vision样本描述变体和EAC3 `dec3` 框变体方面也很脆弱。HLS-fMP4包装器是AVPlayer提供的最宽容的接口;libavformat的 `hls` 复用器产生的字节与 `ffmpeg -f hls -hls_segment_type fmp4` 完全相同,而后者正是Apple HLS规范所定义的。
### Dolby Atmos
EAC3+JOC数据包通过复用器进行流复制,原始的 `dec3` 附加数据得以保留。AVPlayer读取该段,从 `dec3` 框(`numDepSub=1`, `depChanLoc=0x0100`)识别出JOC,并将比特流交给HDMI输出作为Dolby MAT 2.0。AVR点亮Atmos指示灯。引擎在每次Atmos会话时都会发出明确的 `[HLSVideoEngine] EAC3+JOC Atmos: stream-copy engaged, MAT 2.0 passthrough intact` 诊断日志,因此该路径在日志中是明确无误的。
对于fMP4不直接接受的编解码器(TrueHD, DTS, DTS-HD MA),`AudioBridge` 将其解码为PCM并无损地重新编码为FLAC。这保留了5.1 / 7.1环绕声的精确位通道数据,但就定义而言,丢失了空间Atmos / TrueHD-MA对象元数据(这是PCM的衍生)。权衡是基于源的:当包装器能够承载时保留空间混音,当不能时回退到无损5.1 / 7.1。如果由于任何原因,一个JOC源落入了桥接路径,引擎会记录一个醒目的 `WARNING: Atmos downgrade — ...`,这样静默的质量下降就不会被忽略。
## HDR路由
| 源 | 包装器信号传输 |
| ----------------------------------- | ----------------------------------------------------------------- |
| H.264, HEVC (SDR) | BT.709 |
| HEVC Main10 (HDR10) | BT.2020 / PQ |
| HEVC Main10 (HDR10+) | BT.2020 / PQ + 逐帧ST 2094-40 SEI流复制 |
| HEVC Main10 (DV P5 / P8.1 / P8.4) | `dvh1` / `dvhe` 轨道类型并保留源的 `dvcC` 框 |
| HEVC Main10 (HLG) | BT.2020 / HLG |
| AV1 HDR | BT.2020 / PQ |
HDR到SDR的映射由AVPlayer和系统合成器根据连接的显示器处理。AetherEngine不在主机上进行色调映射;它通过HLS-fMP4样本描述告诉系统“这是BT.2020 PQ”(或DV,或HLG),并让tvOS / iOS选择正确的路径。
`DisplayCriteriaController` 在获取第一个段之前,通过 `AVDisplayManager` 发出HDMI内容帧率和动态范围提示,因此在 `AVPlayer` 准备好渲染时,接收器端的握手已经正在进行。
### Dolby Vision信号传输
对于DV流,解复用器会暴露源的 `AVDOVIDecoderConfigurationRecord`。在支持DV的显示器上,`HLSVideoEngine` 将匹配的ISO BMFF `dvcC` 框写入HLS-fMP4样本描述,并为Profile 5, 8.1, 和 8.4发出一个裸的 `dvh1..` 编解码器标签,这样AVKit的自动标准可以从样本条目中读取 `dvh1` 并直接启用DV模式。在非DV显示器上,引擎降级为普通的 `hvc1`:Profile 5在那里不可播放(没有HDR10基础层),Profile 8.1 / 8.4回退到其HDR10 / HLG基础层,并使用AVPlayer的色调映射路径。AV1+DV(Profile 10.0 / 10.1 / 10.4)在支持硬件AV1的主机上使用并行的 `dav1` / `av01` 轨道类型加上 `dvvC` 框。
### HDR10+ 动态元数据
ST 2094-40元数据作为用户数据注册的ITU-T T.35 SEI NAL附加在HEVC比特流中。HLS-fMP4流复制将SEI保留到 `AVPlayer`,后者将其转发给系统合成器。支持HDR10+的电视应用逐场景的色调映射曲线;仅支持HDR10的电视回退到静态HDR10基础。
已发布的 `videoFormat` 对于任何BT.2020 / PQ源以 `.hdr10` 开始,并在生产者扫描中第一次看到数据包的T.35 SEI签名时翻转为 `.hdr10Plus`。在生产者重启之间进行防抖,因此跳转不会重新触发。宿主应用可以根据 `$videoFormat` 的变化来驱动HDR10+徽章或分析钩子。
## 字幕
字幕数据包与音频和视频通过相同的解复用循环路由。没有第二个AVIO连接,没有全文件扫描。每个数据包通过 `avcodec_decode_subtitle2` 内联解码,结果落入一个单一的 `[SubtitleCue]` 已发布列表:
- **文本编解码器** (SubRip / ASS / SSA / WebVTT / mov_text) → `SubtitleCue.body = .text(String)`。ASS对话头和覆盖块(`{\an8}`, `{\b1}`, ...)被剥离;`\N` 变成真实的换行符,以便宿主应用可以使用常规文本布局进行渲染。
- **位图编解码器** (PGS / HDMV PGS / DVB / DVD) → `.image(SubtitleImage)`。索引像素平面通过其调色板遍历,与alpha预乘,并封装为 `CGImage`。位置相对于源视频帧标准化为 `[0..1]`,以便宿主应用可以缩放到任何屏幕矩形。
- **独立文件** (一个单独的 `.srt` / `.ass` / `.vtt` URL) → `selectSidecarSubtitle(url:)` 打开其自己的短期 `AVFormatContext`,一次性解码整个文件,并将结果原子性地交换到 `subtitleCues` 中。
一个携带多个矩形的数据包(PGS经常在顶部发出符号/歌曲,在底部发出对话)会在同一时间范围内变成多个提示,宿主应用渲染所有这些。提示按排序顺序插入;向后跳转根据 `start|end` 去重,因此列表不会在倒带时增长。
宿主应用负责实际的绘制:文本样式、覆盖层布局、淡入淡出过渡、相对于屏幕视频矩形的位置缩放。
## 架构
```
Sources/AetherEngine/
├── AetherEngine.swift Public API + codec dispatch + subtitle stream decode
├── PlayerState.swift PlaybackState, VideoFormat, PlaybackBackend, TrackInfo, SubtitleCue, SubtitleImage
├── Audio/
│ ├── AudioBridge.swift Native path: stream-copy or lossless FLAC transcode per source audio codec
│ ├── AudioDecoder.swift SW path: libavcodec → PCM → CMSampleBuffer with channel-layout tagging
│ └── AudioOutput.swift SW path: AVSampleBufferAudioRenderer + Synchronizer (master clock)
├── Decoder/
│ ├── EmbeddedSubtitleDecoder.swift Inline subtitle decode from demuxed packets
│ ├── SoftwareVideoDecoder.swift SW path: libavcodec/dav1d → CVPixelBuffer (NV12 / P010), HDR10+ side data
│ ├── SubtitleDecoder.swift Sidecar URL one-shot decode (text only)
│ └── VideoDecoderTypes.swift DecodedFrameHandler typealias + VideoDecoderError
├── Demuxer/
│ ├── AVIOReader.swift URLSession → avio_alloc_context
│ └── Demuxer.swift libavformat wrapper
├── Diagnostics/
│ └── EngineLog.swift Gated OSLog emission
├── Display/
│ ├── DisplayCriteriaController.swift AVDisplayManager content-rate / dynamic-range hints (native path)
│ └── FrameRateSnap.swift Snap to standard rates (23.976, 24, 25, 29.97, 30, 50, 59.94, 60)
├── Native/
│ ├── NativeAVPlayerHost.swift Native path: AVPlayer host bound to the loopback HLS-fMP4 URL
│ └── SoftwarePlaybackHost.swift SW path: demux loop + decoders + renderer + synchronizer orchestration
├── Network/
│ └── HLSLocalServer.swift Native path: local HTTP server (127.0.0.1) serving playlist + segments
├── Renderer/
│ └── SampleBufferRenderer.swift SW path: AVSampleBufferDisplayLayer + B-frame reorder, HDR10+ attachments
├── Video/
│ ├── HLSVideoEngine.swift Native path: session orchestrator (muxer wiring, DV signaling, scrub teardown)
│ ├── HLSSegmentProducer.swift Native path: drives libavformat's hls-fmp4 muxer; custom io_open hooks segment writes
│ ├── SegmentCache.swift Native path: producer/consumer segment store with backpressure + scrub-aware eviction
│ └── VTCapabilityProbe.swift VP9 / AV1 system-decode probe (gates codec routing)
└── View/
└── AetherPlayerView.swift Polymorphic surface: hosts either AVPlayerLayer (native) or AVSampleBufferDisplayLayer (SW)
```
## 依赖项
| 包 | 许可证 | 用途 |
| ------------------------------------------------------------------ | --------- | ------------------------------------------------------------------------ |
| [FFmpegBuild](https://github.com/superuser404notfound/FFmpegBuild) | LGPL-3.0 | 精简的FFmpeg 8.1 (avcodec / avformat / avutil / swresample / swscale) 用于解复用 + HLS-fMP4复用 + AudioBridge FLAC编码 + SW路径dav1d解码 + sws_scale YUV → NV12 / P010 |
| VideoToolbox | 系统 | 原生路径视频解码(在可用时使用硬件,在iOS / macOS上使用Apple内置的软件dav1d) |
| AVFoundation | 系统 | AVPlayer + AVDisplayManager (原生路径);AVSampleBufferDisplayLayer + AVSampleBufferRenderSynchronizer (软件路径) |
| CoreMedia | 系统 | 样本描述、格式描述标记、CMTimebase |
## aetherctl 工具
随库一起提供了一个独立的macOS命令行工具,用于在不通过TestFlight + Apple TV的情况下进行重现工作。有三个子命令,都操作于媒体源URL(`file://` 或 `http(s)://`):
```
swift run aetherctl probe # dump container + streams + duration, exit
swift run aetherctl serve # park the engine's loopback HLS-fMP4 server
swift run aetherctl validate # serve + run mediastreamvalidator, exit
swift run aetherctl # alias for serve (backwards compat)
```
`probe` 打开解复用器,打印视频轨道的编解码器 / 分辨率 / 帧率,音频轨道列表(编解码器、通道、语言、Atmos标志),字幕轨道列表,然后退出。不启动HLS服务器。
`serve` 是原始行为。命令行工具打印回环URL并等待直到Ctrl-C;从另一个终端,您可以:
```
curl -i http://127.0.0.1:/master.m3u8
curl -o /tmp/init.mp4 http://127.0.0.1:/init.mp4
mediastreamvalidator http://127.0.0.1:/master.m3u8
mp4dump --verbosity 1 /tmp/init.mp4
ffprobe -v debug /tmp/seg0.mp4
open 'http://127.0.0.1:/master.m3u8' # macOS QuickTime
```
`validate` 是相同的操作,加上针对回环清单的内联 `xcrun mediastreamvalidator` 运行,完成后打印报告并拆卸引擎。
对于可重复运行,`Scripts/fetch-fixtures.sh` 会在 `./Fixtures/` 中生成一小组合成的FFmpeg测试片段(H.264 SDR、HEVC HDR10、AV1、VP9),覆盖原生AVPlayer路径和软件回退路径。真实的DV / Atmos / 多通道源放在 `./Fixtures/user/` 中(已被git忽略)。
## 非目标
AetherEngine有意不做的事情,这样您就无需阅读源码来发现:
- 没有内置UI。没有控件,没有播放进度条,没有美观的HUD。
- 没有分析、遥测或会话报告。请将您自己的连接到 `@Published` 状态。
- 没有播放列表/队列管理。当您想要下一个时,请调用 `load(url:)`。
- 没有字幕覆盖层。引擎解码数据包并发出 `SubtitleCue`(文本或带有标准化位置的 `CGImage`);您的UI使用您想要的任何样式和动画来绘制它们。
- 没有Metal着色器。所有渲染都通过Apple的原生显示栈进行。
- 没有第三方网络。`URLSession` 处理字节;TLS / HTTP-3 / 代理 / MDM规则免费搭载。
## 要求
| | 最低版本 |
| --- | --- |
| iOS | 16.0 |
| tvOS | 16.0 |
| macOS | 14.0 |
| Swift | 6.0 |
| Xcode | 16.0 |
## 使用者
- [Sodalite](https://github.com/superuser404notfound/Sodalite):适用于Apple TV的原生Jellyfin客户端。
## 构建于
AetherEngine是氛围编程、设计并由 [Vincent Herbst](https://github.com/superuser404notfound) 与 **Claude** (Anthropic) 进行紧密结对编程后发布的。提交日志就是收据:几乎每个提交都携带一个 `Co-Authored-By: Claude` 尾部。
## 许可证
[LGPL-3.0,附加Apple Store/DRM例外条款](LICENSE)。例外条款明确授予通过应用程序商店(Apple App Store、TestFlight等)分发的许可,这些商店的条款原本与LGPL §4–6冲突。对引擎本身的修改仍必须在LGPL下发布。标签:Apple平台, Apple生态系统, AVPlayer, dav1d, DNS解析, Dolby Atmos, Dolby Vision, FFmpeg, HDR10, HDR转SDR, HLG, iOS开发, LGPL许可证, libavcodec, macOS开发, Swift编程, tvOS开发, VideoToolbox, 多媒体引擎, 字幕解码, 容器格式支持, 开源项目, 硬件加速解码, 视频播放器, 视频解码, 音频解码, 高性能视频播放