Skip to content

Add playerItem() with automatic video and audio combination - #143

Draft
alexeichhorn wants to merge 25 commits into
mainfrom
feature/player-item
Draft

alexeichhorn wants to merge 25 commits into
mainfrom
feature/player-item

Conversation

@alexeichhorn

@alexeichhorn alexeichhorn commented Aug 19, 2026 •

Copy link
Copy Markdown
Owner
  • Problem: Playing a YouTube video in AVPlayer currently means selecting an already-combined stream, which is often lower quality, or manually pairing separate video and audio streams.
  • Solution: Add playerItem(maxResolution:) to select the best natively playable streams and automatically return one ready-to-use AVPlayerItem. It reuses a suitable combined stream when available, otherwise combines separate high-quality video and audio through the faster HLS-backed path with a composition fallback.

Note

Overview Adds playerItem(maxResolution:), a high-level API that returns an AVPlayerItem using the highest-resolution natively playable media within the requested resolution limit.

Automatic stream combination Prefers an existing combined video-and-audio stream when suitable; otherwise, it pairs the best video-only stream with the highest-bitrate audio stream.

Playback paths Uses native HLS directly for livestreams. For MP4/M4A on-demand media, it builds byte-range HLS playlists from MP4 segment indexes, with AVMutableComposition as the alternate combination path.

Supporting metadata Adds video duration to YouTubeMetadata and carries stream dimensions through local and remote extraction to describe generated playlists and bound playback length. The README now demonstrates the simplified playerItem() workflow.

@alexeichhorn alexeichhorn changed the title Speed up combined player items with HLS Add playerItem() with automatic video and audio combination Aug 19, 2026
throw YouTubeKitError.extractError
}

let metadataDuration = try await metadata?.duration

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not make optional duration metadata a playback prerequisite

This unconditional metadata lookup can throw after streams were retrieved successfully, preventing both the HLS attempt and the composition fallback even though compositionPlayerItem explicitly supports a nil duration. In particular, metadata always uses local extraction regardless of methods, so a .remote stream fallback can still fail whenever local metadata extraction is broken—the exact condition remote extraction is intended to survive.

Comment on lines +35 to +36
if let bestCombinedStream {
if videoStream == nil || (bestCombinedStream.videoResolution ?? 0) >= (videoStream?.videoResolution ?? 0) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Retain the combined stream when no audio-only stream is usable

A lower-resolution combined stream is rejected whenever a higher-resolution video-only stream exists, without considering whether audioStream is nil. If audio-only variants are absent or filtered as non-native, execution then reaches the guard and throws even though bestCombinedStream was directly playable. This can occur when a platform supports the combined stream's audio codec but not the available standalone audio variant.

try videoTrack.insertTimeRange(CMTimeRange(start: .zero, duration: videoTimeRange), of: videoAssetTrack, at: .zero)

// add audio track
try audioTrack.insertTimeRange(CMTimeRange(start: .zero, duration: videoTimeRange), of: audioAssetTrack, at: .zero)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Composition fallback assumes the audio track is as long as the video

The same video-derived time range is inserted from the independent audio asset. Separate YouTube audio and video tracks can have slightly different durations, so when the audio track is shorter, insertTimeRange requests media outside its time range and the promised composition fallback throws instead of producing a player item. This also affects every watchOS call, where the HLS path is excluded.

public func playerItem(maxResolution: Int? = nil) async throws -> AVPlayerItem {
// For livestreams: use the direct url
let isLiveContent = (try? await isLiveContent) ?? false
if isLiveContent, let livestream = try await livestreams.first {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Live streams without an HLS manifest fall into VOD assembly

When content is identified as live but livestreams returns an empty array because its streaming data has no HLS manifest, this condition falls through to ordinary stream selection. The PR also removed checkAvailability()'s prior liveStreamError guard, so a currently live stream—where only HLS is supported—can now be sent through MP4/HLS-index or composition assembly instead of reporting that no supported live stream exists.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intentionally not changing this: falling through can recover a video misclassified as live, while a genuine livestream without HLS will still fail in the existing stream path.

Comment on lines +22 to +23
if isLiveContent, let livestream = try await livestreams.first {
return AVPlayerItem(url: livestream.url)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Livestream playback ignores the resolution cap

The livestream branch returns the native adaptive HLS manifest without applying maxResolution or recording any maximum resolution on the player item. A call such as playerItem(maxResolution: 720) can therefore select a 1080p or 4K HLS variant, violating the public parameter contract and potentially exceeding the caller's bandwidth or device constraint.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 23ee5f6

Comment on lines +21 to +22
let isLiveContent = (try? await isLiveContent) ?? false
if isLiveContent, let livestream = try await livestreams.first {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Livestream playback ignores the selected remote extraction method

This branch calls livestreams, whose implementation is documented to always use local streamingData and ignore methods. Consequently, YouTube(methods: [.remote]).playerItem() can still fail for a livestream when local extraction is broken, even if remote extraction can provide the stream; the parameterized remote test exercises the same local path rather than remote extraction.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changing this in this PR: livestreams already documents that it uses local extraction regardless of methods. Remote livestream extraction can be added separately in the future.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant