fix: external subtitle asset composition (#4830)

* fix: external subtitle asset composition

* fix: filter for supported subtitles before adding them
This commit is contained in:
Kamil Moskała
2026-01-30 22:44:10 +01:00
committed by GitHub
parent ff93ea675e
commit d32406d786
2 changed files with 60 additions and 65 deletions
@@ -15,18 +15,18 @@ extension AVPlayerItem {
if config.externalSubtitles?.isEmpty != false { if config.externalSubtitles?.isEmpty != false {
return AVPlayerItem(asset: asset) return AVPlayerItem(asset: asset)
} }
let supportedExternalSubtitles = config.externalSubtitles?.filter { subtitle in
ExternalSubtitlesUtils.isSubtitleTypeSupported(subtitle: subtitle)
}
if supportedExternalSubtitles?.isEmpty == true {
return AVPlayerItem(asset: asset)
}
if asset.url.pathExtension == "m3u8" { if asset.url.pathExtension == "m3u8" {
let supportedExternalSubtitles = config.externalSubtitles?.filter { subtitle in
ExternalSubtitlesUtils.isSubtitleTypeSupported(subtitle: subtitle)
}
if supportedExternalSubtitles?.isEmpty == true {
return AVPlayerItem(asset: asset)
} else {
return try await ExternalSubtitlesUtils.modifyStreamManifestWithExternalSubtitles( return try await ExternalSubtitlesUtils.modifyStreamManifestWithExternalSubtitles(
for: asset, config: config) for: asset, config: config)
}
} }
return try await ExternalSubtitlesUtils.createCompositionWithExternalSubtitles( return try await ExternalSubtitlesUtils.createCompositionWithExternalSubtitles(
@@ -13,6 +13,7 @@ enum ExternalSubtitlesUtils {
return true return true
} }
print("[ReactNativeVideo] Unsupported external subtitle. Expected VTT. uri: \(subtitle.uri)")
return false return false
} }
@@ -20,73 +21,67 @@ enum ExternalSubtitlesUtils {
for asset: AVURLAsset, for asset: AVURLAsset,
config: NativeVideoConfig config: NativeVideoConfig
) async throws -> AVPlayerItem { ) async throws -> AVPlayerItem {
let subtitlesAssets = try config.externalSubtitles?.map { subtitle in let supportedSubtitles = (config.externalSubtitles ?? []).filter { subtitle in
isSubtitleTypeSupported(subtitle: subtitle)
}
let subtitleAssets: [AVURLAsset] = try supportedSubtitles.map { subtitle in
guard let url = URL(string: subtitle.uri) else { guard let url = URL(string: subtitle.uri) else {
throw PlayerError.invalidTrackUrl(url: subtitle.uri).error() throw PlayerError.invalidTrackUrl(url: subtitle.uri).error()
} }
return AVURLAsset(url: url) return AVURLAsset(url: url)
} }
do { let mainDuration = try await asset.load(.duration)
let mainVideoTracks = asset.tracks(withMediaType: .video)
let mainAudioTracks = asset.tracks(withMediaType: .audio)
let textTracks =
subtitlesAssets?.flatMap { $0.tracks(withMediaType: .text) } ?? []
let composition = AVMutableComposition() let composition = AVMutableComposition()
if let videoTrack = mainVideoTracks.first(where: { $0.mediaType == .video }){ let tracks = try await asset.load(.tracks)
if let compositionVideoTrack = composition.addMutableTrack( for track in tracks {
withMediaType: .video, guard let compTrack = composition.addMutableTrack(
preferredTrackID: kCMPersistentTrackID_Invalid withMediaType: track.mediaType,
) { preferredTrackID: kCMPersistentTrackID_Invalid
try compositionVideoTrack.insertTimeRange( ) else { continue }
CMTimeRange(start: .zero, duration: videoTrack.timeRange.duration),
of: videoTrack, do {
at: .zero try compTrack.insertTimeRange(
) CMTimeRange(start: .zero, duration: mainDuration),
} of: track,
at: .zero
)
} catch {
print("[ReactNativeVideo] Error inserting main track \(track.mediaType.rawValue): \(error.localizedDescription)")
} }
if let audioTrack = mainAudioTracks.first(where: { $0.mediaType == .audio }) {
if let compositionAudioTrack = composition.addMutableTrack(
withMediaType: .audio,
preferredTrackID: kCMPersistentTrackID_Invalid
) {
try compositionAudioTrack.insertTimeRange(
CMTimeRange(start: .zero, duration: audioTrack.timeRange.duration),
of: audioTrack,
at: .zero
)
}
}
for textTrack in textTracks {
if let compositionTextTrack = composition.addMutableTrack(
withMediaType: .text,
preferredTrackID: kCMPersistentTrackID_Invalid
) {
do {
try compositionTextTrack.insertTimeRange(
CMTimeRange(start: .zero, duration: textTrack.timeRange.duration),
of: textTrack,
at: .zero
)
} catch {
print(
"[ReactNativeVideo] Failed to insert text track into composition: \(error.localizedDescription). Language: \(textTrack.languageCode ?? "unknown"). Continuing without this subtitle track."
)
continue
}
compositionTextTrack.languageCode = textTrack.languageCode
compositionTextTrack.isEnabled = true
}
}
return await AVPlayerItem(asset: composition)
} }
for subtitleAsset in subtitleAssets {
let track: AVAssetTrack? = try await subtitleAsset.loadTracks(withMediaType: .text).first
guard let track else { continue }
guard let compSubtitleTrack = composition.addMutableTrack(
withMediaType: track.mediaType,
preferredTrackID: kCMPersistentTrackID_Invalid
) else { continue }
do {
let trackRange = try await track.load(.timeRange)
let effectiveDuration = CMTimeMinimum(trackRange.duration, mainDuration)
try compSubtitleTrack.insertTimeRange(
CMTimeRange(start: .zero, duration: effectiveDuration),
of: track,
at: .zero
)
compSubtitleTrack.languageCode = try await track.load(.languageCode)
compSubtitleTrack.isEnabled = true
} catch {
print("[ReactNativeVideo] Error inserting subtitle track: \(error.localizedDescription)")
continue
}
}
return await AVPlayerItem(asset: composition)
} }
static func modifyStreamManifestWithExternalSubtitles( static func modifyStreamManifestWithExternalSubtitles(