Fix mobile username matching and expose VOD subtitles
This commit is contained in:
@ -6,6 +6,8 @@ class CaptionTracks {
|
||||
this.hls = null;
|
||||
this.probe = null;
|
||||
this.remoteTracks = [];
|
||||
this.timeOffset = 0;
|
||||
this.remoteCueTimes = new WeakMap();
|
||||
this.hlsTracks = new Map();
|
||||
this.retired = new Set();
|
||||
this.generation = 0;
|
||||
@ -48,6 +50,7 @@ class CaptionTracks {
|
||||
this.generation++;
|
||||
this.hls = null;
|
||||
this.probe = null;
|
||||
this.timeOffset = 0;
|
||||
this.retireNativeTracks();
|
||||
this.onChange();
|
||||
}
|
||||
@ -56,6 +59,24 @@ class CaptionTracks {
|
||||
this.probe = { subtitles: info?.subtitles || [], url };
|
||||
}
|
||||
|
||||
setTimeOffset(offset) {
|
||||
this.timeOffset = Number.isFinite(Number(offset)) ? Math.max(0, Number(offset)) : 0;
|
||||
for (const element of this.remoteTracks) this.alignRemoteCues(element);
|
||||
}
|
||||
|
||||
alignRemoteCues(element) {
|
||||
const track = element.track, mode = track.mode;
|
||||
if (!track.cues?.length) return;
|
||||
track.mode = 'hidden';
|
||||
for (const cue of Array.from(track.cues || [])) {
|
||||
if (!this.remoteCueTimes.has(cue)) this.remoteCueTimes.set(cue, { start: cue.startTime, end: cue.endTime });
|
||||
const original = this.remoteCueTimes.get(cue);
|
||||
cue.startTime = Math.max(0, original.start - this.timeOffset);
|
||||
cue.endTime = Math.max(0, original.end - this.timeOffset);
|
||||
}
|
||||
track.mode = mode;
|
||||
}
|
||||
|
||||
prepareMediaRecovery() {
|
||||
const tracks = Array.from(this.hlsTracks, ([key, entry]) => ({ key, entry,
|
||||
cues: Array.from(entry.element.track.cues || []), mode: entry.element.track.mode }));
|
||||
@ -97,6 +118,9 @@ class CaptionTracks {
|
||||
element.label = subtitle.title || subtitle.language || 'Subtitles';
|
||||
element.srclang = subtitle.language || '';
|
||||
element.src = `/api/subtitle?url=${encodeURIComponent(this.probe.url)}&index=${subtitle.index}`;
|
||||
element.addEventListener('load', () => {
|
||||
if (this.remoteTracks.includes(element)) this.alignRemoteCues(element);
|
||||
});
|
||||
this.remoteTracks.push(element);
|
||||
this.video.appendChild(element);
|
||||
}
|
||||
@ -251,6 +275,8 @@ class CaptionTracks {
|
||||
if (this.generation !== generation) return;
|
||||
this.off();
|
||||
track.mode = 'showing';
|
||||
const remote = this.remoteTracks.find(element => element.track === track);
|
||||
if (remote) this.alignRemoteCues(remote);
|
||||
} });
|
||||
}
|
||||
const counts = new Map();
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
/** Metadata probes belong to the active playback, and share an ordered lease across replacements. */
|
||||
class PlaybackProbe {
|
||||
constructor(fetchImpl = fetch) {
|
||||
this.fetch = fetchImpl;
|
||||
this.fetch = (url, options) => fetchImpl(url, options);
|
||||
this.sequence = 0;
|
||||
this.leaseId = Array.from(globalThis.crypto.getRandomValues(new Uint8Array(16)),
|
||||
byte => byte.toString(16).padStart(2, '0')).join('');
|
||||
|
||||
@ -448,6 +448,7 @@ class WatchPage {
|
||||
const info = await (this.playbackProbe ||= new PlaybackProbe(fetch)).read(url, { program, userAgent: ua || '' });
|
||||
if (this.playbackGeneration !== playbackGeneration) return;
|
||||
probeInfo = info;
|
||||
this.getCaptionTracks().setProbe(info, url);
|
||||
this.setSourceDuration(info.duration);
|
||||
console.log(`[WatchPage] Probe result: video=${info.video}, audio=${info.audio}, ${info.width}x${info.height}, compatible=${info.compatible}`);
|
||||
|
||||
@ -484,6 +485,7 @@ class WatchPage {
|
||||
// TODO: Move remux to session logic if seeking is needed for TS files
|
||||
console.log('[WatchPage] Auto: Using remux (.ts container)');
|
||||
this.updateTranscodeStatus('remuxing', 'Remux (Auto)');
|
||||
this.getCaptionTracks().useNative();
|
||||
const finalUrl = this.getRemuxUrl(url, info.audioCodecs || [info.audio], program);
|
||||
this.video.src = finalUrl;
|
||||
this.video.play().catch(e => {
|
||||
@ -531,6 +533,7 @@ class WatchPage {
|
||||
const info = await (this.playbackProbe ||= new PlaybackProbe(fetch)).read(url, { program, userAgent: ua || '' });
|
||||
if (this.playbackGeneration !== playbackGeneration) return;
|
||||
videoCodec = info.video;
|
||||
this.getCaptionTracks().setProbe(info, url);
|
||||
} catch (e) { if (e.name === 'AbortError') return; console.warn('Probe failed for force audio, assuming h264'); }
|
||||
|
||||
if (this.playbackGeneration !== playbackGeneration) return;
|
||||
@ -560,6 +563,8 @@ class WatchPage {
|
||||
}
|
||||
}
|
||||
if (this.playbackGeneration !== playbackGeneration) return;
|
||||
this.getCaptionTracks().setProbe(probeInfo, url);
|
||||
this.getCaptionTracks().useNative();
|
||||
const finalUrl = this.getRemuxUrl(url, probeInfo?.audioCodecs || (probeInfo?.audio ? [probeInfo.audio] : []), program);
|
||||
this.video.src = finalUrl;
|
||||
this.video.play().catch(e => {
|
||||
@ -583,6 +588,7 @@ class WatchPage {
|
||||
} else {
|
||||
// Direct playback for mp4/mkv/avi
|
||||
this.updateTranscodeStatus('direct', 'Direct Play');
|
||||
this.getCaptionTracks().useNative({ allowProbe: !looksLikeHls });
|
||||
this.video.src = finalUrl;
|
||||
this.video.play().catch(e => {
|
||||
if (e.name !== 'AbortError') console.error('[WatchPage] Autoplay error:', e);
|
||||
@ -613,6 +619,7 @@ class WatchPage {
|
||||
this.audioRecovery = new HlsAudioRecovery(hls, this.video, Hls.Events,
|
||||
() => this.hls === hls && this.playbackGeneration === generation,
|
||||
{ beforeRecovery: () => this.getCaptionTracks().prepareMediaRecovery() });
|
||||
this.getCaptionTracks().setTimeOffset(this.currentSessionId ? this.playbackTimeOffset : 0);
|
||||
this.getCaptionTracks().bindHls(hls, Hls.Events);
|
||||
this.hls.loadSource(url);
|
||||
this.hls.attachMedia(this.video);
|
||||
|
||||
Reference in New Issue
Block a user