Files
lan-play/web/js/player.js
T
Ultradesu eb0d4fb1c5
Build and Publish / Build and Publish Docker Image (push) Successful in 10m8s
Init
2026-08-05 12:32:52 +01:00

266 lines
10 KiB
JavaScript

(function () {
class LanPlayPlayer {
constructor(video, wrapper, onError, onLoading, onNext) {
this.video = video;
this.wrapper = wrapper;
this.onError = onError;
this.onLoading = onLoading;
this.onNext = onNext;
this.dashPlayer = null;
this.controlBar = null;
this.sessionId = null;
this.mediaPath = null;
this.dashPreferencesApplied = false;
this.currentEntry = null;
this.nextAvailable = false;
this.restarting = false;
this.operation = 0;
this.freezeFrame = null;
this.playingHandler = null;
this.subtitleUrls = [];
}
async play(entry, startSeconds = 0, preserveFrame = false) {
const framePreserved = preserveFrame && this.captureFrame();
this.onLoading(true, framePreserved);
this.stop({ preserveFrame: framePreserved });
const operation = ++this.operation;
this.currentEntry = entry;
this.mediaPath = entry.path;
this.dashPreferencesApplied = false;
const session = await window.LanPlayApi.startDash(entry.path, startSeconds);
if (operation !== this.operation) {
window.LanPlayApi.stopDash(session.session_id).catch(() => {});
return;
}
this.sessionId = session.session_id;
this.dashPlayer = dashjs.MediaPlayer().create();
this.dashPlayer.updateSettings({
streaming: {
buffer: {
bufferTimeDefault: 8,
bufferTimeAtTopQuality: 8,
bufferTimeAtTopQualityLongForm: 8
}
}
});
this.dashPlayer.initialize(this.video, null, true);
await this.addExternalSubtitles(entry.path, session.start_seconds, operation);
if (operation !== this.operation) return;
this.controlBar = new DashControlBarModule.ControlBar(this.dashPlayer, this.video);
this.controlBar.init(this.wrapper);
this.controlBar.setVodDuration(session.duration_seconds, session.start_seconds);
if (typeof this.controlBar.onSeekRequested === 'function') {
this.controlBar.onSeekRequested((time) => this.seek(time));
}
if (typeof this.controlBar.onNextRequested === 'function') {
this.controlBar.onNextRequested(() => this.onNext());
}
if (typeof this.controlBar.setNextAvailable === 'function') {
this.controlBar.setNextAvailable(this.nextAvailable);
}
this.controlBar.onTrackSelected((type, track) => this.rememberTrack(type, track));
this.controlBar.disable();
const controlBar = this.controlBar;
this.dashPlayer.on(dashjs.MediaPlayer.events.ERROR, (event) => {
if (operation !== this.operation) return;
const message = event && event.error && event.error.message;
this.onError(message || 'DASH playback error');
});
this.dashPlayer.on(dashjs.MediaPlayer.events.STREAM_INITIALIZED, () => {
if (operation !== this.operation || this.controlBar !== controlBar) return;
this.applyDashPreferences();
controlBar.enable();
controlBar.refreshTracks();
});
this.playingHandler = () => {
if (operation !== this.operation) return;
this.playingHandler = null;
this.clearFreezeFrame();
this.onLoading(false, false);
};
this.video.addEventListener('playing', this.playingHandler, { once: true });
this.dashPlayer.attachSource(session.manifest_url);
if (!framePreserved) this.onLoading(false, false);
}
async seek(time) {
if (this.restarting || !this.currentEntry) return;
this.restarting = true;
const entry = this.currentEntry;
try { await this.play(entry, time, true); }
catch (error) {
this.onError(error.message);
this.clearFreezeFrame();
this.onLoading(false, false);
} finally { this.restarting = false; }
}
setNextAvailable(available) {
this.nextAvailable = Boolean(available);
if (this.controlBar && typeof this.controlBar.setNextAvailable === 'function') {
this.controlBar.setNextAvailable(this.nextAvailable);
}
}
applyDashPreferences() {
if (this.dashPreferencesApplied || !this.mediaPath) return;
['audio', 'video'].forEach((type) => {
const tracks = this.dashPlayer.getTracksFor(type) || [];
const preferred = window.LanPlayPreferences.find(this.mediaPath, type, tracks);
if (preferred) this.dashPlayer.setCurrentTrack(preferred);
});
const textTracks = this.dashPlayer.getTracksFor('text') || [];
const preferredText = window.LanPlayPreferences.find(this.mediaPath, 'subtitle', textTracks);
if (preferredText === null) {
this.dashPlayer.setTextTrack(-1);
} else if (preferredText) {
const index = textTracks.indexOf(preferredText);
if (index >= 0) this.dashPlayer.setTextTrack(index);
}
this.dashPreferencesApplied = true;
}
rememberTrack(type, track) {
if (!this.mediaPath) return;
if (type === 'nativeText') {
const tracks = Array.from(this.video.textTracks || []);
window.LanPlayPreferences.remember(this.mediaPath, 'subtitle', track, tracks.indexOf(track));
return;
}
const preferenceType = type === 'text' ? 'subtitle' : type;
const tracks = this.dashPlayer.getTracksFor(type) || [];
window.LanPlayPreferences.remember(this.mediaPath, preferenceType, track, tracks.indexOf(track));
}
async addExternalSubtitles(path, startSeconds = 0, operation = this.operation) {
let subtitles = [];
try { subtitles = await window.LanPlayApi.subtitles(path); } catch (_) { return; }
if (operation !== this.operation) return;
const loaded = await Promise.all(subtitles.filter((item) => item.supported).map(async (subtitle, ordinal) => {
const query = new URLSearchParams({ path, start: String(startSeconds) });
const url = subtitle.source === 'external'
? `/api/subtitles/external/${encodeURIComponent(subtitle.id)}?${query}`
: `/api/subtitles/${subtitle.index}?${query}`;
try {
const response = await fetch(url);
if (!response.ok) return null;
const contents = await response.text();
const objectUrl = URL.createObjectURL(new Blob([contents], { type: 'text/vtt' }));
return { subtitle, ordinal, objectUrl };
} catch (_) {
return null;
}
}));
if (operation !== this.operation) {
loaded.filter(Boolean).forEach((item) => URL.revokeObjectURL(item.objectUrl));
return;
}
loaded.filter(Boolean).forEach(({ subtitle, ordinal, objectUrl }) => {
const label = subtitle.title || subtitle.language || `Subtitle ${ordinal + 1}`;
const idPart = subtitle.source === 'external' ? subtitle.id : subtitle.index;
this.subtitleUrls.push(objectUrl);
this.dashPlayer.addExternalSubtitle(new dashjs.ExternalSubtitle({
id: `lan-play-${subtitle.source}-${idPart}`,
url: objectUrl,
language: label,
mimeType: 'text/vtt',
bandwidth: 256,
periodId: '0'
}));
});
}
captureFrame() {
if (this.video.readyState < 2 || !this.video.videoWidth || !this.video.videoHeight) return false;
try {
this.clearFreezeFrame();
const canvas = document.createElement('canvas');
const width = Math.max(1, this.wrapper.clientWidth);
const height = Math.max(1, this.wrapper.clientHeight);
canvas.width = Math.min(width, 1920);
canvas.height = Math.min(height, 1080);
canvas.className = 'player-freeze-frame';
const context = canvas.getContext('2d');
context.fillStyle = '#000';
context.fillRect(0, 0, canvas.width, canvas.height);
const scale = Math.min(canvas.width / this.video.videoWidth, canvas.height / this.video.videoHeight);
const drawWidth = this.video.videoWidth * scale;
const drawHeight = this.video.videoHeight * scale;
context.drawImage(
this.video,
(canvas.width - drawWidth) / 2,
(canvas.height - drawHeight) / 2,
drawWidth,
drawHeight
);
this.wrapper.appendChild(canvas);
this.freezeFrame = canvas;
return true;
} catch (_) {
this.clearFreezeFrame();
return false;
}
}
clearFreezeFrame() {
if (this.freezeFrame) this.freezeFrame.remove();
this.freezeFrame = null;
}
stop(options = {}) {
this.operation += 1;
if (this.playingHandler) {
this.video.removeEventListener('playing', this.playingHandler);
this.playingHandler = null;
}
if (!options.preserveFrame) this.clearFreezeFrame();
this.revokeSubtitleUrls();
if (this.controlBar) {
this.controlBar.destroy();
this.controlBar = null;
}
if (this.dashPlayer) {
this.dashPlayer.destroy();
this.dashPlayer = null;
}
this.video.pause();
this.video.removeAttribute('src');
while (this.video.firstChild) this.video.removeChild(this.video.firstChild);
this.video.load();
this.mediaPath = null;
this.dashPreferencesApplied = false;
this.currentEntry = null;
if (this.sessionId) {
const sessionId = this.sessionId;
this.sessionId = null;
window.LanPlayApi.stopDash(sessionId).catch(() => {});
}
}
dispose() {
this.operation += 1;
if (this.playingHandler) this.video.removeEventListener('playing', this.playingHandler);
this.playingHandler = null;
this.clearFreezeFrame();
this.revokeSubtitleUrls();
if (this.controlBar) this.controlBar.destroy();
if (this.dashPlayer) this.dashPlayer.destroy();
if (this.sessionId) {
fetch(`/api/dash/${this.sessionId}`, { method: 'DELETE', keepalive: true }).catch(() => {});
}
this.controlBar = null;
this.dashPlayer = null;
this.sessionId = null;
this.currentEntry = null;
}
revokeSubtitleUrls() {
this.subtitleUrls.forEach((url) => URL.revokeObjectURL(url));
this.subtitleUrls = [];
}
}
window.LanPlayPlayer = LanPlayPlayer;
})();