diff --git a/src/HTMLVideo/HTMLVideo.js b/src/HTMLVideo/HTMLVideo.js index a90b068..902b93e 100644 --- a/src/HTMLVideo/HTMLVideo.js +++ b/src/HTMLVideo/HTMLVideo.js @@ -99,6 +99,13 @@ function HTMLVideo(options) { }; containerElement.appendChild(videoElement); + function onFullscreenChanged() { + onPropChanged('fullscreen'); + } + videoElement.addEventListener('webkitbeginfullscreen', onFullscreenChanged); + videoElement.addEventListener('webkitendfullscreen', onFullscreenChanged); + videoElement.addEventListener('fullscreenchange', onFullscreenChanged); + var hls = null; var events = new EventEmitter(); var destroyed = false; @@ -125,7 +132,8 @@ function HTMLVideo(options) { volume: false, muted: false, playbackSpeed: false, - videoScale: false + videoScale: false, + fullscreen: false }; function getProp(propName) { @@ -323,6 +331,12 @@ function HTMLVideo(options) { case 'videoScale': { return videoElement.style.objectFit || 'contain'; } + case 'fullscreen': { + if (stream === null) { + return null; + } + return videoElement.webkitDisplayingFullscreen === true || document.fullscreenElement === videoElement; + } default: { return null; } @@ -550,6 +564,23 @@ function HTMLVideo(options) { break; } + case 'fullscreen': { + if (stream === null) break; + if (propValue) { + if (typeof videoElement.webkitEnterFullscreen === 'function') { + videoElement.webkitEnterFullscreen(); + } else if (typeof videoElement.requestFullscreen === 'function') { + videoElement.requestFullscreen(); + } + } else { + if (typeof videoElement.webkitExitFullscreen === 'function' && videoElement.webkitDisplayingFullscreen) { + videoElement.webkitExitFullscreen(); + } else if (document.fullscreenElement === videoElement) { + document.exitFullscreen(); + } + } + break; + } } } function command(commandName, commandArgs) { @@ -571,6 +602,7 @@ function HTMLVideo(options) { onPropChanged('selectedSubtitlesTrackId'); onPropChanged('audioTracks'); onPropChanged('selectedAudioTrackId'); + onPropChanged('fullscreen'); getContentType(stream) .then(function(contentType) { if (stream !== commandArgs.stream) { @@ -636,6 +668,7 @@ function HTMLVideo(options) { onPropChanged('selectedSubtitlesTrackId'); onPropChanged('audioTracks'); onPropChanged('selectedAudioTrackId'); + onPropChanged('fullscreen'); break; } case 'destroy': { @@ -668,6 +701,9 @@ function HTMLVideo(options) { videoElement.onvolumechange = null; videoElement.onratechange = null; videoElement.textTracks.onchange = null; + videoElement.removeEventListener('webkitbeginfullscreen', onFullscreenChanged); + videoElement.removeEventListener('webkitendfullscreen', onFullscreenChanged); + videoElement.removeEventListener('fullscreenchange', onFullscreenChanged); containerElement.removeChild(videoElement); containerElement.removeChild(styleElement); break; @@ -727,7 +763,7 @@ HTMLVideo.canPlayStream = function(stream) { HTMLVideo.manifest = { name: 'HTMLVideo', external: false, - props: ['stream', 'loaded', 'paused', 'time', 'duration', 'buffering', 'buffered', 'audioTracks', 'selectedAudioTrackId', 'subtitlesTracks', 'selectedSubtitlesTrackId', 'subtitlesOffset', 'subtitlesSize', 'subtitlesTextColor', 'subtitlesBackgroundColor', 'subtitlesOutlineColor', 'subtitlesOpacity', 'volume', 'muted', 'playbackSpeed', 'videoScale'], + props: ['stream', 'loaded', 'paused', 'time', 'duration', 'buffering', 'buffered', 'audioTracks', 'selectedAudioTrackId', 'subtitlesTracks', 'selectedSubtitlesTrackId', 'subtitlesOffset', 'subtitlesSize', 'subtitlesTextColor', 'subtitlesBackgroundColor', 'subtitlesOutlineColor', 'subtitlesOpacity', 'volume', 'muted', 'playbackSpeed', 'videoScale', 'fullscreen'], commands: ['load', 'unload', 'destroy'], events: ['propValue', 'propChanged', 'ended', 'error', 'subtitlesTrackLoaded', 'audioTrackLoaded'] }; diff --git a/src/withHTMLSubtitles/withHTMLSubtitles.js b/src/withHTMLSubtitles/withHTMLSubtitles.js index df42804..9bc2f3f 100644 --- a/src/withHTMLSubtitles/withHTMLSubtitles.js +++ b/src/withHTMLSubtitles/withHTMLSubtitles.js @@ -40,6 +40,63 @@ function withHTMLSubtitles(Video) { containerElement.style.zIndex = '0'; containerElement.appendChild(subtitlesElement); + var videoElement = containerElement.querySelector('video'); + var nativeTextTrack = null; + var syntheticNativeTextTracks = []; + + function createNativeTrack() { + removeNativeTrack(); + if (cuesByTime === null || selectedTrackId === null) return false; + var selectedTrack = tracks.find(function(track) { return track.id === selectedTrackId; }); + if (!selectedTrack) return false; + var delayMs = delay || 0; + nativeTextTrack = videoElement.addTextTrack('subtitles', selectedTrack.label || selectedTrack.lang, selectedTrack.lang || ''); + syntheticNativeTextTracks.push(nativeTextTrack); + cuesByTime.times.forEach(function(time) { + cuesByTime[time].forEach(function(cue) { + if (cue.startTime !== time) return; + var start = (cue.startTime + delayMs) / 1000; + var end = (cue.endTime + delayMs) / 1000; + if (start < 0) start = 0; + if (end <= start) return; + nativeTextTrack.addCue(new VTTCue(start, end, cue.text)); + }); + }); + nativeTextTrack.mode = 'showing'; + return true; + } + function removeNativeTrack() { + if (nativeTextTrack !== null) { + nativeTextTrack.mode = 'disabled'; + nativeTextTrack = null; + } + } + function isNativeTextTrack(track) { + return syntheticNativeTextTracks.includes(track); + } + function getEmbeddedTrackIndex(trackId) { + if (typeof trackId !== 'string' || !trackId.startsWith('EMBEDDED_')) { + return null; + } + var index = parseInt(trackId.replace('EMBEDDED_', ''), 10); + return isNaN(index) ? null : index; + } + function isWebkitDisplayingFullscreen() { + return videoElement && videoElement.webkitDisplayingFullscreen === true; + } + function onWebkitBeginFullscreen() { + createNativeTrack(); + subtitlesElement.style.display = 'none'; + } + function onWebkitEndFullscreen() { + removeNativeTrack(); + subtitlesElement.style.display = ''; + } + if (videoElement) { + videoElement.addEventListener('webkitbeginfullscreen', onWebkitBeginFullscreen); + videoElement.addEventListener('webkitendfullscreen', onWebkitEndFullscreen); + } + var videoState = { time: null, paused: false, @@ -190,7 +247,7 @@ function withHTMLSubtitles(Video) { events.emit(eventName, propName, getProp(propName, propValue)); - if (propName === 'selectedSubtitlesTrackId' && propValue !== null && selectedTrackId !== null) { + if (propName === 'selectedSubtitlesTrackId' && propValue !== null && selectedTrackId !== null && nativeTextTrack === null) { setProp('selectedExtraSubtitlesTrackId', null); } } @@ -276,6 +333,24 @@ function withHTMLSubtitles(Video) { return opacity; } + case 'subtitlesTracks': { + if (Array.isArray(videoPropValue) && videoElement && videoElement.textTracks) { + return videoPropValue.filter(function(track) { + var index = getEmbeddedTrackIndex(track.id); + return index === null || !isNativeTextTrack(videoElement.textTracks[index]); + }); + } + + return videoPropValue; + } + case 'selectedSubtitlesTrackId': { + if (typeof videoPropValue === 'string' && videoElement && videoElement.textTracks) { + var index = getEmbeddedTrackIndex(videoPropValue); + return index !== null && isNativeTextTrack(videoElement.textTracks[index]) ? null : videoPropValue; + } + + return videoPropValue; + } default: { return videoPropValue; } @@ -369,6 +444,9 @@ function withHTMLSubtitles(Video) { cuesByTime = result; startRenderLoop(); + if (isWebkitDisplayingFullscreen() && nativeTextTrack === null) { + createNativeTrack(); + } events.emit('extraSubtitlesTrackLoaded', selectedTrack); }) .catch(function(error) { @@ -556,6 +634,7 @@ function withHTMLSubtitles(Video) { return false; } case 'unload': { + removeNativeTrack(); stopRenderLoop(); lastTimeIndex = null; cuesByTime = null; @@ -571,6 +650,10 @@ function withHTMLSubtitles(Video) { case 'destroy': { command('unload'); destroyed = true; + if (videoElement) { + videoElement.removeEventListener('webkitbeginfullscreen', onWebkitBeginFullscreen); + videoElement.removeEventListener('webkitendfullscreen', onWebkitEndFullscreen); + } onPropChanged('extraSubtitlesSize'); onPropChanged('extraSubtitlesOffset'); onPropChanged('extraSubtitlesTextColor');