266 lines
10 KiB
JavaScript
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;
|
|
})();
|