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, 多媒体引擎, 字幕解码, 容器格式支持, 开源项目, 硬件加速解码, 视频播放器, 视频解码, 音频解码, 高性能视频播放