From 752ab8dc8a65e9a880f73e44e8d94736d981b65b Mon Sep 17 00:00:00 2001 From: Jos Vooges | STH Date: Mon, 28 Sep 2026 20:30:33 +0200 Subject: [PATCH] Send Chrome User-Agent and Referer on Viaplay CDN fetches from Android, iOS and Tizen players. --- .../mediacluster/tv/playback/PlayerFactory.kt | 27 +++++- .../mediacluster/tv/ui/EventPlayerScreen.kt | 30 ++++-- .../mediacluster/tv/playback/PlayerFactory.kt | 24 ++++- .../mediacluster/tv/ui/EventPlayerScreen.kt | 8 +- apps/ios/VMC/Playback/ClearKeyDashProxy.swift | 12 ++- .../VMC/Playback/VLCPlayerController.swift | 96 +++++++++++++++++-- .../VMCTV/Playback/VLCPlayerController.swift | 10 ++ apps/tizen-tv/www/js/player.js | 27 +++++- 8 files changed, 205 insertions(+), 29 deletions(-) diff --git a/apps/android-tv/app-mobile/src/main/java/nl/vonas/mediacluster/tv/playback/PlayerFactory.kt b/apps/android-tv/app-mobile/src/main/java/nl/vonas/mediacluster/tv/playback/PlayerFactory.kt index d4b6bb4..287e618 100644 --- a/apps/android-tv/app-mobile/src/main/java/nl/vonas/mediacluster/tv/playback/PlayerFactory.kt +++ b/apps/android-tv/app-mobile/src/main/java/nl/vonas/mediacluster/tv/playback/PlayerFactory.kt @@ -39,6 +39,7 @@ import androidx.media3.exoplayer.DefaultRenderersFactory.EXTENSION_RENDERER_MODE import androidx.media3.exoplayer.DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER import nl.vonas.mediacluster.tv.data.ClearKeyEntry import nl.vonas.mediacluster.tv.data.DrmInfo +import okhttp3.Interceptor import okhttp3.OkHttpClient import java.nio.ByteBuffer import java.util.UUID @@ -190,11 +191,32 @@ fun buildTvPlayer(context: Context): ExoPlayer { } /** Gedeelde HTTP-client voor HLS-segmenten (connectie-hergebruik). */ +private const val VIAPLAY_BROWSER_UA = + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36" + +private fun isViaplayCdnHost(host: String): Boolean = + host.contains("viaplay", ignoreCase = true) + +/** Viaplay-CDN geeft Don't go terug op app-UA's; zelfde headers als de server-play. */ +private val viaplayBrowserHeaders = Interceptor { chain -> + val req = chain.request() + if (!isViaplayCdnHost(req.url.host)) return@Interceptor chain.proceed(req) + chain.proceed( + req.newBuilder() + .header("User-Agent", VIAPLAY_BROWSER_UA) + .header("Referer", "https://viaplay.com/") + .header("Origin", "https://viaplay.com") + .header("Accept", "*/*") + .build(), + ) +} + private val liveOkHttp: OkHttpClient by lazy { OkHttpClient.Builder() .connectTimeout(15, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .retryOnConnectionFailure(true) + .addInterceptor(viaplayBrowserHeaders) .build() } @@ -334,6 +356,7 @@ fun liveMediaItem( url: String, format: String, drm: DrmInfo? = null, + live: Boolean = true, ): MediaItem { // URL wint: ESPN/FAAB levert .m3u8 terwijl de API soms format=dash hardcodet. val isHls = url.contains("m3u8", ignoreCase = true) || @@ -347,7 +370,7 @@ fun liveMediaItem( else -> MimeTypes.VIDEO_MP2T } val builder = MediaItem.Builder().setUri(url).setMimeType(mime) - if (isHls) { + if (live && isHls) { builder.setLiveConfiguration( MediaItem.LiveConfiguration.Builder() .setTargetOffsetMs(2_000) @@ -356,7 +379,7 @@ fun liveMediaItem( .build(), ) } - if (isDash) { + if (live && isDash && !url.contains("/start/") && !url.contains("/stop/")) { builder.setLiveConfiguration( MediaItem.LiveConfiguration.Builder() .setTargetOffsetMs(3_000) diff --git a/apps/android-tv/app-mobile/src/main/java/nl/vonas/mediacluster/tv/ui/EventPlayerScreen.kt b/apps/android-tv/app-mobile/src/main/java/nl/vonas/mediacluster/tv/ui/EventPlayerScreen.kt index 3696f1e..50b2e71 100644 --- a/apps/android-tv/app-mobile/src/main/java/nl/vonas/mediacluster/tv/ui/EventPlayerScreen.kt +++ b/apps/android-tv/app-mobile/src/main/java/nl/vonas/mediacluster/tv/ui/EventPlayerScreen.kt @@ -88,6 +88,8 @@ fun EventPlayerScreen( api: ApiClient, eventId: String, onBack: () -> Unit, + hubPlay: Boolean = false, + productUrl: String? = null, ) { val context = LocalContext.current val playerRef = remember { AtomicReference(null) } @@ -132,7 +134,7 @@ fun EventPlayerScreen( } } - LaunchedEffect(eventId) { + LaunchedEffect(eventId, hubPlay, productUrl) { loading = true error = null watchSessionId = null @@ -142,7 +144,19 @@ fun EventPlayerScreen( audioTracks = emptyList() textTracks = emptyList() try { - val play = api.scheduleEventPlay(eventId) + val play = when { + hubPlay && eventId.startsWith("viaplay-content:", ignoreCase = true) -> { + val guid = eventId.removePrefix("viaplay-content:").removePrefix("VIAPLAY-CONTENT:") + api.viaplayContentPlay(guid, productUrl) + } + hubPlay -> { + val contentId = Regex("""(?i)^f1tv:(\d+)$""").find(eventId.trim()) + ?.groupValues?.getOrNull(1) + ?: eventId.trim().removePrefix("f1tv:").removePrefix("F1TV:") + api.f1tvContentPlay(contentId) + } + else -> api.scheduleEventPlay(eventId) + } if (!isActive) return@LaunchedEffect title = play.name watchSessionId = play.watchSessionId @@ -151,14 +165,14 @@ fun EventPlayerScreen( error = "Geen stream-URL beschikbaar" return@LaunchedEffect } - if (play.drm == null || play.drm.keys.isEmpty()) { - error = "Encryptiesleutels ontbreken — stream start binnenkort" - return@LaunchedEffect - } + val kapWindow = url.contains("/start/") && url.contains("/stop/") + val asVod = play.live == false || + kapWindow || + (hubPlay && eventId.startsWith("viaplay-content:", ignoreCase = true)) val item = try { - liveMediaItem(url, play.format, play.drm) + liveMediaItem(url, play.format, play.drm, live = !asVod) } catch (e: Exception) { - error = e.message ?: "Ongeldige DRM-sleutels" + error = e.message ?: "Kon mediastream niet openen" return@LaunchedEffect } if (!isActive) return@LaunchedEffect diff --git a/apps/android-tv/app/src/main/java/nl/vonas/mediacluster/tv/playback/PlayerFactory.kt b/apps/android-tv/app/src/main/java/nl/vonas/mediacluster/tv/playback/PlayerFactory.kt index 703d996..55ef78f 100644 --- a/apps/android-tv/app/src/main/java/nl/vonas/mediacluster/tv/playback/PlayerFactory.kt +++ b/apps/android-tv/app/src/main/java/nl/vonas/mediacluster/tv/playback/PlayerFactory.kt @@ -43,6 +43,7 @@ import io.github.anilbeesetti.nextlib.media3ext.ffdecoder.NextRenderersFactory import androidx.media3.exoplayer.DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF import androidx.media3.exoplayer.DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON import androidx.media3.exoplayer.DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER +import okhttp3.Interceptor import okhttp3.OkHttpClient import java.util.UUID import java.util.concurrent.ConcurrentHashMap @@ -199,11 +200,32 @@ fun buildTvPlayer(context: Context, audioPassthrough: Boolean = false): ExoPlaye } /** Gedeelde HTTP-client voor HLS-segmenten (connectie-hergebruik). */ +private const val VIAPLAY_BROWSER_UA = + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36" + +private fun isViaplayCdnHost(host: String): Boolean = + host.contains("viaplay", ignoreCase = true) + +/** Viaplay-CDN geeft Don't go terug op app-UA's; zelfde headers als de server-play. */ +private val viaplayBrowserHeaders = Interceptor { chain -> + val req = chain.request() + if (!isViaplayCdnHost(req.url.host)) return@Interceptor chain.proceed(req) + chain.proceed( + req.newBuilder() + .header("User-Agent", VIAPLAY_BROWSER_UA) + .header("Referer", "https://viaplay.com/") + .header("Origin", "https://viaplay.com") + .header("Accept", "*/*") + .build(), + ) +} + private val liveOkHttp: OkHttpClient by lazy { OkHttpClient.Builder() .connectTimeout(15, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .retryOnConnectionFailure(true) + .addInterceptor(viaplayBrowserHeaders) .build() } @@ -414,7 +436,7 @@ fun liveMediaItem( .build(), ) } - if (live && isDash) { + if (live && isDash && !url.contains("/start/") && !url.contains("/stop/")) { builder.setLiveConfiguration( MediaItem.LiveConfiguration.Builder() .setTargetOffsetMs(3_000) diff --git a/apps/android-tv/app/src/main/java/nl/vonas/mediacluster/tv/ui/EventPlayerScreen.kt b/apps/android-tv/app/src/main/java/nl/vonas/mediacluster/tv/ui/EventPlayerScreen.kt index 45b0ed0..f120407 100644 --- a/apps/android-tv/app/src/main/java/nl/vonas/mediacluster/tv/ui/EventPlayerScreen.kt +++ b/apps/android-tv/app/src/main/java/nl/vonas/mediacluster/tv/ui/EventPlayerScreen.kt @@ -170,8 +170,11 @@ fun EventPlayerScreen( return@LaunchedEffect } // ClearKey optioneel: F1TV plain HLS heeft drm=null. - // VOD/clips: geen live-config — anders seek naar “live edge” = eindscherm. - val asVod = hubPlay && eventId.startsWith("viaplay-content:", ignoreCase = true) + // Catch-up/VOD: geen live-config — anders seek naar live-edge. + val kapWindow = url.contains("/start/") && url.contains("/stop/") + val asVod = play.live == false || + kapWindow || + (hubPlay && eventId.startsWith("viaplay-content:", ignoreCase = true)) val item = try { liveMediaItem(url, play.format, play.drm, live = !asVod) } catch (e: Exception) { @@ -350,6 +353,7 @@ fun EventPlayerScreen( factory = { ctx -> PlayerView(ctx).apply { useController = false + keepScreenOn = true layoutParams = ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, diff --git a/apps/ios/VMC/Playback/ClearKeyDashProxy.swift b/apps/ios/VMC/Playback/ClearKeyDashProxy.swift index e8b7e96..f205c1e 100644 --- a/apps/ios/VMC/Playback/ClearKeyDashProxy.swift +++ b/apps/ios/VMC/Playback/ClearKeyDashProxy.swift @@ -1047,7 +1047,17 @@ final class ClearKeyDashProxy { var req = URLRequest(url: url) req.timeoutInterval = 15 req.cachePolicy = .reloadIgnoringLocalCacheData - req.setValue("VMC-iOS-ClearKey/0.5.5", forHTTPHeaderField: "User-Agent") + let host = url.host?.lowercased() ?? "" + if host.contains("viaplay") { + req.setValue( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36", + forHTTPHeaderField: "User-Agent" + ) + req.setValue("https://viaplay.com/", forHTTPHeaderField: "Referer") + req.setValue("https://viaplay.com", forHTTPHeaderField: "Origin") + } else { + req.setValue("VMC-iOS-ClearKey/0.5.5", forHTTPHeaderField: "User-Agent") + } req.setValue("bytes", forHTTPHeaderField: "Accept-Ranges") let (data, resp) = try await urlSession.data(for: req) if let http = resp as? HTTPURLResponse, !(200...299).contains(http.statusCode) { diff --git a/apps/ios/VMC/Playback/VLCPlayerController.swift b/apps/ios/VMC/Playback/VLCPlayerController.swift index ed22df2..8733204 100644 --- a/apps/ios/VMC/Playback/VLCPlayerController.swift +++ b/apps/ios/VMC/Playback/VLCPlayerController.swift @@ -58,6 +58,8 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg private var knownVodDurationMs: Int64 = 0 /// Playlist is geknipt vanaf dit punt (VLC-tijd 0 = deze offset). private var seekOriginMs: Int64 = 0 + /// Taal/naam van het gekozen audiospoor — na seek-reload terugzetten (VLC pakt anders weer Spaans). + private var restoreAudioHint: String = "" override init() { // Shared library = alle demux/access plugins (DASH adaptive in VLC 4). @@ -93,6 +95,11 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg drawableHost.attach(to: view) return } + // VOD: PiP-host zetten vóór VLC een gewone vout opent (anders komt pictureInPictureReady nooit). + if wantsPipDrawable, mediaPlayer.media == nil, view.bounds.width > 1 { + applyDrawable(force: true) + return + } if mediaPlayer.drawable as AnyObject? === view { return } @@ -118,27 +125,31 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg drawableHost.attach(to: view) return } + if wantsPipDrawable, mediaPlayer.media == nil { + applyDrawable(force: true) + return + } if mediaPlayer.drawable == nil { applyDrawable(force: true) } } + private var wantsPipDrawable: Bool { !liveMode && !clearKeyProxyActive && !stopping } + /// Live/ClearKey → plain UIView. VOD → VLCPipDrawableHost (PiP vanaf start). private func applyDrawable(force: Bool = false) { guard !stopping else { return } guard let view = plainVideoView else { return } // Nooit binden op 0×0 — VLC opent dan vout zonder beeld (audio-only). guard view.bounds.width > 1, view.bounds.height > 1 else { return } - let wantPipHost = !liveMode && !clearKeyProxyActive + let wantPipHost = wantsPipDrawable usePipDrawable = wantPipHost if wantPipHost { drawableHost.attach(to: view) if force || mediaPlayer.drawable as AnyObject? !== drawableHost { mediaPlayer.drawable = drawableHost } - if pipWindow != nil { - pipPossible = AVPictureInPictureController.isPictureInPictureSupported() - } + pipPossible = AVPictureInPictureController.isPictureInPictureSupported() } else { // ClearKey/live: plain UIView. Nooit detach/rebind als al gezet tijdens play. if mediaPlayer.drawable as AnyObject? === view, !force { return } @@ -188,9 +199,10 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg else { startPictureInPicture() } } - func play(url: URL, startMs: Int64 = 0, subtitleFileURL: URL? = nil) { + func play(url: URL, startMs: Int64 = 0, subtitleFileURL: URL? = nil) async { // Annuleer pending finishStop van eerdere/spurious onDisappear. stopGeneration &+= 1 + let playGen = stopGeneration stopping = false hasError = false errorMessage = nil @@ -199,12 +211,18 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg audioTracks = [] canSeek = false didApplyPreferredAudio = false + restoreAudioHint = "" knownVodDurationMs = 0 seekOriginMs = 0 liveMode = false clearKeyProxyActive = false - plainVideoView?.layoutIfNeeded() - applyDrawable(force: true) + await waitForSurface(timeout: 2.0) + guard stopGeneration == playGen, !stopping else { return } + await MainActor.run { + plainVideoView?.layoutIfNeeded() + applyDrawable(force: true) + } + guard stopGeneration == playGen, !stopping else { return } pipPossible = AVPictureInPictureController.isPictureInPictureSupported() guard let media = VLCMedia(url: url) else { hasError = true @@ -221,6 +239,9 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg media.addOption(":freetype-rel-fontsize=\(Self.subtitleRelFontSize)") media.addOption(":freetype-bold=1") media.addOption(":sub-margin=0") + if mediaPlayer.drawable as AnyObject? !== drawableHost { + applyDrawable(force: true) + } mediaPlayer.media = media _ = VLCMediaParser.shared().queue( media, @@ -267,6 +288,7 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg liveStartedAt = Date() canSeek = false didApplyPreferredAudio = false + restoreAudioHint = "" knownVodDurationMs = 0 seekOriginMs = 0 liveMode = true @@ -436,8 +458,13 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg media.addOption(":http-reconnect") media.addOption(":avcodec-hw=any") media.addOption(":freetype-rel-fontsize=\(subtitleRelFontSize)") - // IPTV/CDN's weigeren soms exotic UA's. - media.addOption(":http-user-agent=VLC/3.0.21 LibVLC/3.0.21") + let viaplay = url.host?.localizedCaseInsensitiveContains("viaplay") == true + if viaplay && !viaLocalProxy { + media.addOption(":http-user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36") + media.addOption(":http-referrer=https://viaplay.com/") + } else { + media.addOption(":http-user-agent=VLC/3.0.21 LibVLC/3.0.21") + } // VLC 4: unified adaptive demux (DASH+HLS). Forceer GEEN :demux=dash (VLC3-module). media.addOption(":adaptive-logic=highest") if !viaLocalProxy { @@ -472,6 +499,8 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg if let url = comps?.url { position = knownVodDurationMs > 0 ? Float(Double(clamped) / Double(knownVodDurationMs)) : 0 timeText = Self.format(ms: Int32(min(clamped, Int64(Int32.max)))) + captureAudioRestoreHint() + didApplyPreferredAudio = false startLiveMedia(url: url, format: livePlayFormat) invalidatePipPlaybackState() return @@ -516,6 +545,7 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg if let track = mediaPlayer.audioTracks.first(where: { $0.trackId == id }) { track.isSelectedExclusively = true currentAudioId = id + restoreAudioHint = Self.audioHint(language: track.language, name: track.trackName, description: track.trackDescription) } invalidatePipPlaybackState() } @@ -592,10 +622,36 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg applyPreferredAudioIfNeeded() } + private func captureAudioRestoreHint() { + if let track = mediaPlayer.audioTracks.first(where: { $0.isSelected || $0.trackId == currentAudioId }) { + restoreAudioHint = Self.audioHint(language: track.language, name: track.trackName, description: track.trackDescription) + return + } + if restoreAudioHint.isEmpty, let name = audioTracks.first(where: { $0.id == currentAudioId })?.name { + restoreAudioHint = name.lowercased() + } + } + private func applyPreferredAudioIfNeeded() { - guard !didApplyPreferredAudio else { return } let list = mediaPlayer.audioTracks guard !list.isEmpty else { return } + if !restoreAudioHint.isEmpty { + let hint = restoreAudioHint + if let match = list.max(by: { + Self.audioHintScore(language: $0.language, name: $0.trackName, description: $0.trackDescription, hint: hint) + < Self.audioHintScore(language: $1.language, name: $1.trackName, description: $1.trackDescription, hint: hint) + }), + Self.audioHintScore(language: match.language, name: match.trackName, description: match.trackDescription, hint: hint) > 0 + { + if !match.isSelected { + match.isSelectedExclusively = true + } + currentAudioId = match.trackId + didApplyPreferredAudio = true + return + } + } + guard !didApplyPreferredAudio else { return } didApplyPreferredAudio = true let ranked = list.enumerated().map { i, t in (track: t, score: Self.audioPreferenceScore(language: t.language, name: t.trackName, description: t.trackDescription, index: i)) @@ -605,6 +661,26 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg best.track.isSelectedExclusively = true } currentAudioId = best.track.trackId + restoreAudioHint = Self.audioHint(language: best.track.language, name: best.track.trackName, description: best.track.trackDescription) + } + + private static func audioHint(language: String?, name: String?, description: String?) -> String { + [language, name, description] + .compactMap { $0?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() } + .filter { !$0.isEmpty } + .joined(separator: " ") + } + + private static func audioHintScore(language: String?, name: String?, description: String?, hint: String) -> Int { + let blob = audioHint(language: language, name: name, description: description) + if blob.isEmpty || hint.isEmpty { return 0 } + if blob == hint { return 100 } + let lang = (language ?? "").lowercased() + if !lang.isEmpty, hint.contains(lang) { return 80 } + let trackName = (name ?? "").lowercased() + if !trackName.isEmpty, hint.contains(trackName) { return 70 } + let tokens = hint.split(whereSeparator: { !$0.isLetter }).map(String.init).filter { $0.count >= 2 } + return tokens.contains(where: { blob.contains($0) }) ? 40 : 0 } /// NL eerst, anders Engels. Geen match → 0 (niet forceren). diff --git a/apps/ios/VMCTV/Playback/VLCPlayerController.swift b/apps/ios/VMCTV/Playback/VLCPlayerController.swift index 7cd958d..7d732ce 100644 --- a/apps/ios/VMCTV/Playback/VLCPlayerController.swift +++ b/apps/ios/VMCTV/Playback/VLCPlayerController.swift @@ -20,6 +20,13 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg private var lengthMs: Int32 = 0 private var tick: AnyCancellable? + private static func applyCdnHeaders(to media: VLCMedia, url: URL) { + if url.host?.localizedCaseInsensitiveContains("viaplay") == true { + media.addOption(":http-user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36") + media.addOption(":http-referrer=https://viaplay.com/") + } + } + override init() { super.init() mediaPlayer.delegate = self @@ -44,6 +51,7 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg media.addOption(":sub-track=0") media.addOption(":freetype-rel-fontsize=\(Self.subtitleRelFontSize)") media.addOption(":freetype-bold=1") + Self.applyCdnHeaders(to: media, url: url) mediaPlayer.media = media media.parse() mediaPlayer.play() @@ -75,6 +83,7 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg media.addOption(":http-reconnect") media.addOption(":live-caching=1000") media.addOption(":freetype-rel-fontsize=\(Self.subtitleRelFontSize)") + Self.applyCdnHeaders(to: media, url: url) mediaPlayer.media = media media.parse() mediaPlayer.play() @@ -87,6 +96,7 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg fb.addOption(":network-caching=1200") fb.addOption(":http-reconnect") fb.addOption(":freetype-rel-fontsize=\(Self.subtitleRelFontSize)") + Self.applyCdnHeaders(to: fb, url: fallback) self.mediaPlayer.media = fb fb.parse() self.mediaPlayer.play() diff --git a/apps/tizen-tv/www/js/player.js b/apps/tizen-tv/www/js/player.js index 526484a..54884cb 100644 --- a/apps/tizen-tv/www/js/player.js +++ b/apps/tizen-tv/www/js/player.js @@ -13,6 +13,7 @@ var lastAvSubText = ""; var playSession = 0; var fallbackOnce = false; + var pendingStartMs = 0; function $(id) { return document.getElementById(id); @@ -228,9 +229,20 @@ setSubOverlay(""); } + function applyPendingStart() { + var ms = pendingStartMs; + if (!ms || ms < 5000) return; + pendingStartMs = 0; + try { + if (usingAvplay) webapis.avplay.seekTo(ms); + else if (htmlVideo) htmlVideo.currentTime = ms / 1000; + } catch (e) {} + } + function stop() { playSession += 1; fallbackOnce = false; + pendingStartMs = 0; stopTick(); setError(null); clearExternal(); @@ -275,6 +287,7 @@ if (session !== playSession) return; setError(null); setPlayingIcon(true); + applyPendingStart(); }; htmlVideo.onended = function () { if (session !== playSession) return; @@ -368,10 +381,12 @@ webapis.avplay.setDisplayRect(0, 0, 1920, 1080); try { - webapis.avplay.setStreamingProperty( - "USER_AGENT", - "Mozilla/5.0 (SMART-TV; Linux; Tizen 6.0) AppleWebKit/537.36" - ); + var ua = "Mozilla/5.0 (SMART-TV; Linux; Tizen 6.0) AppleWebKit/537.36"; + if (/viaplay/i.test(url)) { + ua = + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36"; + } + webapis.avplay.setStreamingProperty("USER_AGENT", ua); } catch (e2) {} webapis.avplay.setListener({ @@ -417,6 +432,7 @@ return; } webapis.avplay.play(); + applyPendingStart(); startTick(); setError(null); setPlayingIcon(true); @@ -437,10 +453,11 @@ }, 80); } - function playUrl(url, title) { + function playUrl(url, title, startMs) { stop(); playSession += 1; fallbackOnce = false; + pendingStartMs = startMs > 5000 ? startMs : 0; currentUrl = url; currentTitle = title || ""; $("player-title").textContent = currentTitle;