Send Chrome User-Agent and Referer on Viaplay CDN fetches from Android, iOS and Tizen players.
This commit is contained in:
parent
75969d87ec
commit
752ab8dc8a
8 changed files with 205 additions and 29 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<ExoPlayer?>(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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue