From 71f0d15a760e97ce2b981ef90b26f08c7de55290 Mon Sep 17 00:00:00 2001 From: Raffi Date: Wed, 3 Dec 2025 17:39:46 +0100 Subject: [PATCH 1/8] refactor to use network api --- .../AVSubtitlesLoader.swift | 192 ++++++++---------- .../Parsers/MasterPlaylistParser.swift | 33 ++- .../Parsers/PlaylistParser.swift | 20 +- .../Parsers/VariantPlaylistParser.swift | 31 ++- 4 files changed, 118 insertions(+), 158 deletions(-) diff --git a/Code/Sideloaded-TextTracks/Sources/THEOplayerConnectorSideloadedSubtitle/AVSubtitlesLoader.swift b/Code/Sideloaded-TextTracks/Sources/THEOplayerConnectorSideloadedSubtitle/AVSubtitlesLoader.swift index 1479d301..65142dee 100644 --- a/Code/Sideloaded-TextTracks/Sources/THEOplayerConnectorSideloadedSubtitle/AVSubtitlesLoader.swift +++ b/Code/Sideloaded-TextTracks/Sources/THEOplayerConnectorSideloadedSubtitle/AVSubtitlesLoader.swift @@ -10,89 +10,64 @@ import AVFoundation import THEOplayerSDK class AVSubtitlesLoader: NSObject { + private static var instances: [AVSubtitlesLoader] = [] + static func addInstance(_ loader: AVSubtitlesLoader) { Self.instances.append(loader) } + static func removeInstance(by id: String) { + Self.instances.removeAll { $0._id == id } + } + static var cachingInstance: AVSubtitlesLoader? // can be removed when adding multi-cachingtask support + private let subtitles: [TextTrackDescription] - private(set) var variantTotalDuration: Double = 0 private let transformer = SubtitlesTransformer() private let synchronizer: SubtitlesSynchronizer? + private let _id: String + private var variantTotalDuration: Double = 0 - init(subtitles: [TextTrackDescription], player: THEOplayer?) { + init(subtitles: [TextTrackDescription], id: String, player: THEOplayer?) { self.subtitles = subtitles + self._id = id self.synchronizer = SubtitlesSynchronizer(player: player) self.synchronizer?.delegate = self.transformer + + super.init() + + _ = player?.addEventListener(type: PlayerEventTypes.DESTROY, listener: { [weak self] destroyEvent in self?.handleDestroyEvent() }) } - - func handleMasterManifestRequest(_ request: AVAssetResourceLoadingRequest) -> Bool { - guard let originalURL = request.request.url?.withScheme(newScheme: URLScheme.https) else { - return false - } - - MasterPlaylistParser(url: originalURL).sideLoadSubtitles(subtitles: subtitles) { data in - guard let masterManifestData = data else { - print("[AVSubtitlesLoader] ERROR: Couldn't find manifest data") - request.finishLoading(with: URLError(URLError.cannotParseResponse)) - return - } - let response = HTTPURLResponse(url: originalURL, statusCode: 200, httpVersion: nil, headerFields: nil) - request.response = response - request.dataRequest?.respond(with: masterManifestData) - request.finishLoading() + + func handleMasterManifestRequest(_ url: URL) async -> Data? { + let parser = MasterPlaylistParser(url: url) + + guard let responseData = await parser.sideLoadSubtitles(subtitles: subtitles) else { + print("[AVSubtitlesLoader] ERROR: Couldn't find manifest data") + return nil } - return true + + return responseData } - func handleVariantManifest(_ request: AVAssetResourceLoadingRequest) -> Bool { - guard let customSchemeURL = request.request.url, - let originalURLString = customSchemeURL.absoluteString.byRemovingScheme(scheme: URLScheme.variantm3u8), - let originalURL = URL(string:originalURLString) else { - print("[AVSubtitlesLoader] ERROR: Variant manifest is invalid") - request.finishLoading(with: URLError(URLError.unsupportedURL)) - return false - } - - VariantPlaylistParser(url: originalURL).parse { playlist in - guard let playlist = playlist, let responseData = playlist.manifestData else { - print("[AVSubtitlesLoader] ERROR: Couldn't find variant data") - request.finishLoading(with: URLError(URLError.cannotParseResponse)) - return - } - self.variantTotalDuration = playlist.totalPlayListDuration - let response = HTTPURLResponse(url: originalURL, statusCode: 200, httpVersion: nil, headerFields: nil) - request.response = response - request.dataRequest?.respond(with: responseData) - request.finishLoading() + func handleVariantManifest(_ url: URL) async -> Data? { + let parser = VariantPlaylistParser(url: url) + + guard let playlist = await parser.parse(), + let responseData = playlist.manifestData else { + print("[AVSubtitlesLoader] ERROR: Couldn't find variant data") + return nil } - return true + + self.variantTotalDuration = playlist.totalPlayListDuration + return responseData } - - func handleSubtitles(_ request: AVAssetResourceLoadingRequest) -> Bool { - guard let customSchemeURL = request.request.url else { - return false - } - - guard let originalURLString = customSchemeURL.absoluteString.byRemovingScheme(scheme: URLScheme.subtitlesm3u8), - let originalURL = URL(string: originalURLString) else { - print("[AVSubtitlesLoader] ERROR: Failed to revert subtitle URL!") - return false - } - - let subtitlem3u8 = self.getSubtitleManifest(for: originalURL) + + func handleSubtitles(_ url: URL) -> Data? { + let subtitlem3u8 = self.getSubtitleManifest(for: url) if THEOplayerConnectorSideloadedSubtitle.SHOW_DEBUG_LOGS { print("[AVSubtitlesLoader] SUBTITLE: +++++++") print(subtitlem3u8) print("[AVSubtitlesLoader] SUBTITLE: ------") } - - guard let data = subtitlem3u8.data(using: .utf8) else { - return false - } - - let response = HTTPURLResponse(url: originalURL, statusCode: 200, httpVersion: nil, headerFields: nil) - request.response = response - request.dataRequest?.respond(with: data) - request.finishLoading() - - return true + + return subtitlem3u8.data(using: .utf8) } fileprivate func getSubtitleManifest(for originalURL: URL) -> String { @@ -132,6 +107,10 @@ class AVSubtitlesLoader: NSObject { } return track } + + private func handleDestroyEvent() { + Self.removeInstance(by: _id) + } } enum URLScheme: String { @@ -151,48 +130,40 @@ enum URLScheme: String { } } -extension AVSubtitlesLoader: ManifestInterceptor { - var customScheme: String { - //the initial interception scheme - URLScheme.masterm3u8.urlScheme - } - - func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool { +extension AVSubtitlesLoader: MediaPlaylistInterceptor { + func shouldInterceptPlaylistRequest(type: HlsPlaylistType) -> Bool { false } + func didInterceptPlaylistRequest(type: HlsPlaylistType, request: URLRequest) async throws -> URLRequest { request } + + func failedToPerformURLRequest(request: URLRequest, response: URLResponse) { if THEOplayerConnectorSideloadedSubtitle.SHOW_DEBUG_LOGS { - print("[AVSubtitlesLoader] loadingRequest", loadingRequest.request.url?.absoluteString ?? "") + print("[AVSubtitlesLoader] failedToPerformURLRequest", request.url?.absoluteString ?? "") } - return intercept(loadingRequest: loadingRequest) - } - - func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForRenewalOfRequestedResource renewalRequest: AVAssetResourceRenewalRequest) -> Bool { + + func shouldInterceptPlaylistResponse(type: HlsPlaylistType) -> Bool { true } + func didInterceptPlaylistResponse(type: HlsPlaylistType, url: URL, response: URLResponse, data: Data) async throws -> Data { if THEOplayerConnectorSideloadedSubtitle.SHOW_DEBUG_LOGS { - print("[AVSubtitlesLoader] renewalRequest", renewalRequest.request.url?.absoluteString ?? "") + print("[AVSubtitlesLoader] intercept url", url.absoluteString, self) } - return intercept(loadingRequest: renewalRequest) + return await interceptResponse(type: type, url: url, data: data) } - - private func intercept(loadingRequest: AVAssetResourceLoadingRequest) -> Bool { - guard let scheme = loadingRequest.request.url?.scheme else { - return false - } - switch scheme { - case URLScheme.masterm3u8.name: + + private func interceptResponse(type: HlsPlaylistType, url: URL, data: Data) async -> Data { + switch type { + case .master : // intercept the master manifest to append the subtitles - return self.handleMasterManifestRequest(loadingRequest) - case URLScheme.variantm3u8.name: + return await self.handleMasterManifestRequest(url) ?? data + case .video: // intercept the variant manifest to get the duration - return self.handleVariantManifest(loadingRequest) - case URLScheme.subtitlesm3u8.name: + return await self.handleVariantManifest(url) ?? data + case .subtitles: // intercept the subtitle request to respond with the HLS subtitle - return self.handleSubtitles(loadingRequest) + return self.handleSubtitles(url) ?? data default: break } - - return false + return data } - } extension THEOplayer { @@ -202,17 +173,18 @@ extension THEOplayer { - Remark: - Once used this method, always use it to set a source (even if there are no sideloaded subtitles in it), otherwise the subtitle helper logic can break the playback behavior */ - public func setSourceWithSubtitles(source: SourceDescription?){ - - if let source = source { - if let sideLoadedTextTracks = SourceValidator.getValidTextTracks(source) { - let subtitleLoader = AVSubtitlesLoader(subtitles: sideLoadedTextTracks, player: self) - self.developerSettings?.manifestInterceptor = subtitleLoader - } else { - self.developerSettings?.manifestInterceptor = nil - } + public func setSourceWithSubtitles(source: SourceDescription?) { + if let source = source, + let sideLoadedTextTracks = SourceValidator.getValidTextTracks(source) { + let loader = AVSubtitlesLoader( + subtitles: sideLoadedTextTracks, + id: String(self.uid), + player: self + ) + AVSubtitlesLoader.addInstance(loader) + self.network.addMediaPlaylistInterceptor(loader) } else { - self.developerSettings?.manifestInterceptor = nil + AVSubtitlesLoader.removeInstance(by: String(self.uid)) } self.source = source @@ -228,14 +200,20 @@ extension Cache { - Once used this method, always use it to cache a source (even if there are no sideloaded subtitles in it), otherwise the subtitle helper logic can break the caching behavior */ public func createTaskWithSubtitles(source: SourceDescription, parameters: CachingParameters?) -> CachingTask? { + guard let cachingTask = createTask(source: source, parameters: parameters) else { return nil } if let sideLoadedTextTracks = SourceValidator.getValidTextTracks(source) { - let subtitleLoader = AVSubtitlesLoader(subtitles: sideLoadedTextTracks, player: nil) - self.developerSettings?.manifestInterceptor = subtitleLoader + let loader = AVSubtitlesLoader( + subtitles: sideLoadedTextTracks, + id: cachingTask.id, + player: nil + ) + AVSubtitlesLoader.cachingInstance = loader + self.network.addMediaPlaylistInterceptor(loader) } else { - self.developerSettings?.manifestInterceptor = nil + AVSubtitlesLoader.cachingInstance = nil } - return createTask(source: source, parameters: parameters) + return cachingTask } } #endif diff --git a/Code/Sideloaded-TextTracks/Sources/THEOplayerConnectorSideloadedSubtitle/Parsers/MasterPlaylistParser.swift b/Code/Sideloaded-TextTracks/Sources/THEOplayerConnectorSideloadedSubtitle/Parsers/MasterPlaylistParser.swift index 218aa558..292fcdb5 100644 --- a/Code/Sideloaded-TextTracks/Sources/THEOplayerConnectorSideloadedSubtitle/Parsers/MasterPlaylistParser.swift +++ b/Code/Sideloaded-TextTracks/Sources/THEOplayerConnectorSideloadedSubtitle/Parsers/MasterPlaylistParser.swift @@ -17,24 +17,17 @@ class MasterPlaylistParser: PlaylistParser { super.init(url: url) } - func sideLoadSubtitles(subtitles: [TextTrackDescription], completion: @escaping (_ data: Data?) -> ()) { - self.loadManifest { succ in - if succ { - self.parseManifest() - self.appendSubtitlesLines(subtitles: subtitles) - let constructed = self.constructedManifestArray.joined(separator: "\n") - - if THEOplayerConnectorSideloadedSubtitle.SHOW_DEBUG_LOGS { - print("[AVSubtitlesLoader] MASTER: +++++++") - print(constructed) - print("[AVSubtitlesLoader] MASTER: ------") - } - - completion(constructed.data(using: .utf8)) - } else { - completion(nil) - } + func sideLoadSubtitles(subtitles: [TextTrackDescription]) async -> Data? { + guard let _ = await self.loadManifest() else { return nil } + self.parseManifest() + self.appendSubtitlesLines(subtitles: subtitles) + let constructed = self.constructedManifestArray.joined(separator: "\n") + if THEOplayerConnectorSideloadedSubtitle.SHOW_DEBUG_LOGS { + print("[AVSubtitlesLoader] MASTER: +++++++") + print(constructed) + print("[AVSubtitlesLoader] MASTER: ------") } + return constructed.data(using: .utf8) } fileprivate func parseManifest() { @@ -83,8 +76,8 @@ class MasterPlaylistParser: PlaylistParser { func appendSubtitlesLines(subtitles: [TextTrackDescription]) { for subtitle in subtitles { if let label = subtitle.label, let encodedURLString = subtitle.src.absoluteString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) { - let subtitleCustomSchemePath = encodedURLString.byConcatenatingScheme(scheme: URLScheme.subtitlesm3u8) - let subtitleLine = "#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"\(self.subtitlesGroupId)\",NAME=\"\(label)\",URI=\"\(subtitleCustomSchemePath)\",LANGUAGE=\"\(subtitle.srclang)\"" + let defaultValue = subtitle.isDefault == true ? "YES" : "NO" + let subtitleLine = "#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"\(self.subtitlesGroupId)\",NAME=\"\(label)\",DEFAULT=\(defaultValue),URI=\"\(encodedURLString)\",LANGUAGE=\"\(subtitle.srclang)\"" if let linePosition = self.lastMediaLine { self.constructedManifestArray.insert("\(subtitleLine)", at: linePosition) } else { @@ -98,6 +91,6 @@ class MasterPlaylistParser: PlaylistParser { guard let variantURL = self.getFullURL(from: path) else { return path.trimmingCharacters(in: .whitespacesAndNewlines) } - return variantURL.absoluteString.byConcatenatingScheme(scheme: URLScheme.variantm3u8) + return variantURL.absoluteString } } diff --git a/Code/Sideloaded-TextTracks/Sources/THEOplayerConnectorSideloadedSubtitle/Parsers/PlaylistParser.swift b/Code/Sideloaded-TextTracks/Sources/THEOplayerConnectorSideloadedSubtitle/Parsers/PlaylistParser.swift index 8867623e..d50dbf81 100644 --- a/Code/Sideloaded-TextTracks/Sources/THEOplayerConnectorSideloadedSubtitle/Parsers/PlaylistParser.swift +++ b/Code/Sideloaded-TextTracks/Sources/THEOplayerConnectorSideloadedSubtitle/Parsers/PlaylistParser.swift @@ -16,24 +16,20 @@ class PlaylistParser { self.manifestData = nil } - func loadManifest(completion: @escaping (_ success: Bool) -> ()) { - URLSession.shared.dataTask(with: self.manifestURL) { [weak self] data, response, error in - guard let responseData = data, let self = self else { - completion(false) - return - } + func loadManifest() async -> Data? { + if let (data, response) = try? await URLSession.shared.data(from: self.manifestURL) { // Update the manifestUrl to the url received in the response (to pickup possible url redirect) - if let responseUrl = response?.url { + if let responseUrl = response.url { self.manifestURL = responseUrl } - if self.isValidManifest(data: responseData) { - self.manifestData = responseData - completion(true) + if self.isValidManifest(data: data) { + self.manifestData = data + return data } else { - completion(false) + return nil } } - .resume() + return nil } func isValidManifest(data: Data) -> Bool { diff --git a/Code/Sideloaded-TextTracks/Sources/THEOplayerConnectorSideloadedSubtitle/Parsers/VariantPlaylistParser.swift b/Code/Sideloaded-TextTracks/Sources/THEOplayerConnectorSideloadedSubtitle/Parsers/VariantPlaylistParser.swift index e7f27322..72adf59d 100644 --- a/Code/Sideloaded-TextTracks/Sources/THEOplayerConnectorSideloadedSubtitle/Parsers/VariantPlaylistParser.swift +++ b/Code/Sideloaded-TextTracks/Sources/THEOplayerConnectorSideloadedSubtitle/Parsers/VariantPlaylistParser.swift @@ -16,26 +16,19 @@ class VariantPlaylistParser: PlaylistParser { super.init(url: url) } - func parse(completion: @escaping (_ playlist: VariantPlaylistParser?) -> ()) { - self.loadManifest { succ in - if succ { - self.parseManifest() - let constructed = self.constructedManifestArray.joined(separator: "\n") - - if THEOplayerConnectorSideloadedSubtitle.SHOW_DEBUG_LOGS { - print("[AVSubtitlesLoader] VARIANT: +++++++") - print(constructed) - print("[AVSubtitlesLoader] VARIANT: ------") - } - - if let data = constructed.data(using: .utf8) { - self.manifestData = data - } - completion(self) - } else { - completion(nil) - } + func parse() async -> VariantPlaylistParser? { + guard let _ = await self.loadManifest() else { return nil } + self.parseManifest() + let constructed = self.constructedManifestArray.joined(separator: "\n") + if THEOplayerConnectorSideloadedSubtitle.SHOW_DEBUG_LOGS { + print("[AVSubtitlesLoader] VARIANT: +++++++") + print(constructed) + print("[AVSubtitlesLoader] VARIANT: ------") + } + if let data = constructed.data(using: .utf8) { + self.manifestData = data } + return self } fileprivate func parseManifest() { From fab99525755ef9503837e74f191f8faa713727f1 Mon Sep 17 00:00:00 2001 From: Raffi Date: Wed, 3 Dec 2025 17:43:00 +0100 Subject: [PATCH 2/8] bump theoplayer sdk dependency version --- THEOplayer-Connector-SideloadedSubtitle.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/THEOplayer-Connector-SideloadedSubtitle.podspec b/THEOplayer-Connector-SideloadedSubtitle.podspec index 4bdd9a1c..06daa043 100644 --- a/THEOplayer-Connector-SideloadedSubtitle.podspec +++ b/THEOplayer-Connector-SideloadedSubtitle.podspec @@ -19,7 +19,7 @@ Pod::Spec.new do |s| s.static_framework = true s.swift_versions = ['5.3', '5.4', '5.5', '5.6', '5.7'] - s.dependency 'THEOplayerSDK-core', "~> 10" + s.dependency 'THEOplayerSDK-core', "~> 10.7" s.dependency 'SwiftSubtitles', '0.9.1' s.dependency 'Swifter', '1.5.0' end From ea5610633f3ffc0319d1ea9935f9608b4a6c3c35 Mon Sep 17 00:00:00 2001 From: Raffi Date: Wed, 3 Dec 2025 17:50:28 +0100 Subject: [PATCH 3/8] add changelog --- CHANGELOG.md | 5 +++++ Code/Sideloaded-TextTracks/README.md | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e9083e9..917bade0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- SideloadedSubtitle + - Added support to make a sideloaded subtitle selected for caching by default. Use the `isDefault` property in `SSTextTrackDescription` or `TextTrackDescription`. + ## [10.7.0] - 2025-12-18 ### Changed diff --git a/Code/Sideloaded-TextTracks/README.md b/Code/Sideloaded-TextTracks/README.md index 8b546f95..ca3fddfb 100644 --- a/Code/Sideloaded-TextTracks/README.md +++ b/Code/Sideloaded-TextTracks/README.md @@ -171,7 +171,7 @@ All that is needed is a source with a text track description: ```swift public static var sourceWithSideloadedTextTrack: SourceDescription { let typedSource = TypedSource(src: "https://sourceURL.com/manifest.m3u8, type: "application/x-mpegurl") - let textTrack = TextTrackDescription(src: "https://sideloadedurl.com/subtitle.vtt", srclang: "language_code", isDefault: false, kind: .subtitles, label: "Label", format: .WebVTT) + let textTrack = TextTrackDescription(src: "https://sideloadedurl.com/subtitle.vtt", srclang: "language_code", isDefault: true, kind: .subtitles, label: "Label", format: .WebVTT) return SourceDescription(source: typedSource, textTracks: [textTrack]) } ``` From 228249f699323da08810e417ae18934d74e431cf Mon Sep 17 00:00:00 2001 From: Raffi Date: Thu, 4 Dec 2025 15:57:37 +0100 Subject: [PATCH 4/8] use caching task interceptor --- .../AVSubtitlesLoader.swift | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/Code/Sideloaded-TextTracks/Sources/THEOplayerConnectorSideloadedSubtitle/AVSubtitlesLoader.swift b/Code/Sideloaded-TextTracks/Sources/THEOplayerConnectorSideloadedSubtitle/AVSubtitlesLoader.swift index 65142dee..73cad29f 100644 --- a/Code/Sideloaded-TextTracks/Sources/THEOplayerConnectorSideloadedSubtitle/AVSubtitlesLoader.swift +++ b/Code/Sideloaded-TextTracks/Sources/THEOplayerConnectorSideloadedSubtitle/AVSubtitlesLoader.swift @@ -15,7 +15,6 @@ class AVSubtitlesLoader: NSObject { static func removeInstance(by id: String) { Self.instances.removeAll { $0._id == id } } - static var cachingInstance: AVSubtitlesLoader? // can be removed when adding multi-cachingtask support private let subtitles: [TextTrackDescription] private let transformer = SubtitlesTransformer() @@ -23,7 +22,7 @@ class AVSubtitlesLoader: NSObject { private let _id: String private var variantTotalDuration: Double = 0 - init(subtitles: [TextTrackDescription], id: String, player: THEOplayer?) { + init(subtitles: [TextTrackDescription], id: String, player: THEOplayer? = nil, cachingTask: CachingTask? = nil) { self.subtitles = subtitles self._id = id self.synchronizer = SubtitlesSynchronizer(player: player) @@ -32,6 +31,7 @@ class AVSubtitlesLoader: NSObject { super.init() _ = player?.addEventListener(type: PlayerEventTypes.DESTROY, listener: { [weak self] destroyEvent in self?.handleDestroyEvent() }) + _ = cachingTask?.addEventListener(type: CachingTaskEventTypes.STATE_CHANGE, listener: { [weak self] cachingTaskStateChangeEvent in self?.handleCachingTaskStateChangeEvent(task: cachingTask) }) } func handleMasterManifestRequest(_ url: URL) async -> Data? { @@ -111,6 +111,12 @@ class AVSubtitlesLoader: NSObject { private func handleDestroyEvent() { Self.removeInstance(by: _id) } + + private func handleCachingTaskStateChangeEvent(task: CachingTask?) { + guard let task, + task.status == .evicted else { return } + Self.removeInstance(by: task.id) + } } enum URLScheme: String { @@ -205,12 +211,10 @@ extension Cache { let loader = AVSubtitlesLoader( subtitles: sideLoadedTextTracks, id: cachingTask.id, - player: nil + cachingTask: cachingTask ) - AVSubtitlesLoader.cachingInstance = loader - self.network.addMediaPlaylistInterceptor(loader) - } else { - AVSubtitlesLoader.cachingInstance = nil + AVSubtitlesLoader.addInstance(loader) + cachingTask.network.addMediaPlaylistInterceptor(loader) } return cachingTask From f5eb932a17ee6d49188e4807550812f4196262bc Mon Sep 17 00:00:00 2001 From: Raffi Date: Thu, 4 Dec 2025 16:00:01 +0100 Subject: [PATCH 5/8] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 917bade0..14a8c617 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - SideloadedSubtitle + - Added support for simultaneous multi-source caching with sideloaded subtitles. Previously there was a limitation of caching only a single task at a time. - Added support to make a sideloaded subtitle selected for caching by default. Use the `isDefault` property in `SSTextTrackDescription` or `TextTrackDescription`. ## [10.7.0] - 2025-12-18 From e126723f5f8a8a8848a2f72142bc0073b1daf082 Mon Sep 17 00:00:00 2001 From: Raffi Date: Wed, 10 Dec 2025 11:25:26 +0100 Subject: [PATCH 6/8] rm limitation doc --- Code/Sideloaded-TextTracks/README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Code/Sideloaded-TextTracks/README.md b/Code/Sideloaded-TextTracks/README.md index ca3fddfb..278a504f 100644 --- a/Code/Sideloaded-TextTracks/README.md +++ b/Code/Sideloaded-TextTracks/README.md @@ -188,5 +188,4 @@ For more information on how to implement offline playback with caching, please r ### Limitations -1. Caching sources with sideloaded subtitles can only be done one task at a time. This is due to some technical complexities in the underlying implementation. This limitation may be addressed in future releases. -2. Caching is only available on iOS. +1. Caching is only available on iOS. From c2fd5f00c32721fec0bdacddb9dcfe243b2bb9bb Mon Sep 17 00:00:00 2001 From: Raffi Date: Wed, 7 Jan 2026 16:29:39 +0100 Subject: [PATCH 7/8] update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14a8c617..f5d00e38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - SideloadedSubtitle - Added support for simultaneous multi-source caching with sideloaded subtitles. Previously there was a limitation of caching only a single task at a time. - - Added support to make a sideloaded subtitle selected for caching by default. Use the `isDefault` property in `SSTextTrackDescription` or `TextTrackDescription`. + - Added support to make a sideloaded subtitle selected for caching by default. Use the `isDefault` property in `SSTextTrackDescription` or `TextTrackDescription`. Only one default track can be added. ## [10.7.0] - 2025-12-18 From 61085ee4d57881d20bf7e59938b2bbb2f7eff177 Mon Sep 17 00:00:00 2001 From: Raffi Date: Fri, 9 Jan 2026 16:12:25 +0100 Subject: [PATCH 8/8] mv findTrackDescription --- .../AVSubtitlesLoader.swift | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Code/Sideloaded-TextTracks/Sources/THEOplayerConnectorSideloadedSubtitle/AVSubtitlesLoader.swift b/Code/Sideloaded-TextTracks/Sources/THEOplayerConnectorSideloadedSubtitle/AVSubtitlesLoader.swift index 73cad29f..ecc75f7a 100644 --- a/Code/Sideloaded-TextTracks/Sources/THEOplayerConnectorSideloadedSubtitle/AVSubtitlesLoader.swift +++ b/Code/Sideloaded-TextTracks/Sources/THEOplayerConnectorSideloadedSubtitle/AVSubtitlesLoader.swift @@ -59,7 +59,11 @@ class AVSubtitlesLoader: NSObject { } func handleSubtitles(_ url: URL) -> Data? { - let subtitlem3u8 = self.getSubtitleManifest(for: url) + guard let trackDescription: THEOplayerSDK.TextTrackDescription = self.findTrackDescription(by: url) else { + return nil + } + + let subtitlem3u8 = self.getSubtitleManifest(for: url, trackDescription: trackDescription) if THEOplayerConnectorSideloadedSubtitle.SHOW_DEBUG_LOGS { print("[AVSubtitlesLoader] SUBTITLE: +++++++") @@ -70,9 +74,8 @@ class AVSubtitlesLoader: NSObject { return subtitlem3u8.data(using: .utf8) } - fileprivate func getSubtitleManifest(for originalURL: URL) -> String { - let trackDescription: THEOplayerSDK.TextTrackDescription? = self.findTrackDescription(by: originalURL) - let format: THEOplayerSDK.TextTrackFormat = trackDescription?.format ?? .WebVTT + fileprivate func getSubtitleManifest(for originalURL: URL, trackDescription: THEOplayerSDK.TextTrackDescription) -> String { + let format: THEOplayerSDK.TextTrackFormat = trackDescription.format ?? .WebVTT let timestamp: SSTextTrackDescription.WebVttTimestamp? = (trackDescription as? SSTextTrackDescription)?.vttTimestamp let autosync: Bool? = (trackDescription as? SSTextTrackDescription)?.automaticTimestampSyncEnabled let subtitlesMediaURL: String