#include utility.js #include uniqstr.js #include watcher.js const MszVideoEmbedPlayerEvents = function() { return [ 'play', 'pause', 'stop', 'mute', 'volume', 'rate', 'duration', 'time', ]; }; const MszVideoEstimateAspectRatio = function(width, height) { const gcd = function(a, b) { return b ? gcd(b, a % b) : Math.abs(a); }; const ratio = gcd(width, height); width /= ratio; height /= ratio; return [width, height]; }; const MszVideoConstrainSize = function(w, h, mw, mh) { mw = mw || 0; mh = mh || 0; w = w || 0; h = h || 0; const ar = MszVideoEstimateAspectRatio(w, h); if(w > h) { if(mw > 0) { w = Math.min(mw, w); h = Math.ceil((w / ar[0]) * ar[1]); if(mh > 0) h = Math.min(mh, h); } } else { if(mh > 0) { h = Math.min(mh, h); w = Math.ceil((h / ar[1]) * ar[0]); if(mw > 0) w = Math.min(mw, w); } } return [w, h]; }; const MszVideoEmbed = function(playerOrFrame) { const frame = playerOrFrame; const player = 'getPlayer' in frame ? frame.getPlayer() : frame; const elem = $e({ attrs: { classList: ['embed', 'embed-' + player.getType()], }, child: frame, }); return { getElement: function() { return elem; }, appendTo: function(target) { target.appendChild(elem); }, insertBefore: function(ref) { $ib(ref, elem); }, nuke: function() { $r(elem); }, replaceElement(target) { $ib(target, elem); $r(target); }, getFrame: function() { return frame; }, getPlayer: function() { return player; }, }; }; const MszVideoEmbedFrame = function(player, options) { options = options || {}; const icoStatePlay = 'fa-play', icoStatePause = 'fa-pause', icoStateStop = 'fa-stop'; const icoVolMute = 'fa-volume-mute', icoVolOff = 'fa-volume-off', icoVolQuiet = 'fa-volume-down', icoVolLoud = 'fa-volume-up'; const btnPlayPause = $e({ attrs: {}, child: { tag: 'i', attrs: { classList: ['fas', 'fa-fw', icoStatePlay], }, } }); const btnStop = $e({ attrs: {}, child: { tag: 'i', attrs: { classList: ['fas', 'fa-fw', icoStateStop], }, }, }); const numCurrentTime = $e({ attrs: {}, }); const sldProgress = $e({ attrs: {}, child: [], }); const numDurationRemaining = $e({ attrs: {}, }); const btnVolMute = $e({ attrs: {}, child: { tag: 'i', attrs: { // isMuted === icoVolMute // vol < 0.01 === icoVolOff // vol < 0.5 === icoVolQuiet // vol < 1.0 === icoVolLoud classList: ['fas', 'fa-fw', icoVolLoud], }, }, }); const elem = $e({ attrs: { className: 'embedvf', style: { width: player.getWidth().toString() + 'px', height: player.getHeight().toString() + 'px', }, }, child: [ { attrs: { className: 'embedvf-player', }, child: player, }, { attrs: { className: 'embedvf-overlay', }, child: [ { attrs: { className: 'embedvf-controls', }, child: [ btnPlayPause, btnStop, numCurrentTime, sldProgress, numDurationRemaining, ], }, ], }, ], }); return { getElement: function() { return elem; }, appendTo: function(target) { target.appendChild(elem); }, insertBefore: function(ref) { $ib(ref, elem); }, nuke: function() { $r(elem); }, replaceElement(target) { $ib(target, elem); $r(target); }, getPlayer: function() { return player; }, }; }; const MszVideoEmbedPlayer = function(metadata, options) { options = options || {}; const shouldAutoplay = options.autoplay === undefined || options.autoplay, haveNativeControls = options.nativeControls !== undefined && options.nativeControls, shouldObserveResize = options.observeResize === undefined || options.observeResize; const videoAttrs = { src: metadata.url, style: {}, }; if(shouldAutoplay) videoAttrs.autoplay = 'autoplay'; if(haveNativeControls) videoAttrs.controls = 'controls'; const constrainSize = function(w, h, mw, mh) { return MszVideoConstrainSize(w, h, mw || options.maxWidth, mh || options.maxHeight); }; const initialSize = constrainSize( options.width || metadata.width || 200, options.height || metadata.height || 200 ); videoAttrs.style.width = initialSize[0].toString() + 'px'; videoAttrs.style.height = initialSize[1].toString() + 'px'; const watchers = new MszWatcherCollection; watchers.define(MszVideoEmbedPlayerEvents()); const player = $e({ tag: 'video', attrs: videoAttrs, }); const setSize = function(w, h) { const size = constrainSize(w, h, initialSize[0], initialSize[1]); player.style.width = size[0].toString() + 'px'; player.style.height = size[1].toString() + 'px'; }; const pub = { getElement: function() { return player; }, appendTo: function(target) { target.appendChild(player); }, insertBefore: function(ref) { $ib(ref, player); }, nuke: function() { $r(player); }, replaceElement(target) { $ib(target, player); $r(target); }, getType: function() { return 'external'; }, getWidth: function() { return width; }, getHeight: function() { return height; }, }; pub.watch = (name, handler) => watchers.watch(name, handler); pub.unwatch = (name, handler) => watchers.unwatch(name, handler); if(shouldObserveResize) player.addEventListener('resize', function() { setSize(player.videoWidth, player.videoHeight); }); player.addEventListener('play', function() { watchers.call('play'); }); const pPlay = function() { player.play(); }; pub.play = pPlay; const pPause = function() { player.pause(); }; pub.pause = pPause; let stopCalled = false; player.addEventListener('pause', function() { watchers.call(stopCalled ? 'stop' : 'pause'); stopCalled = false; }); const pStop = function() { stopCalled = true; player.pause(); player.currentTime = 0; }; pub.stop = pStop; const pIsPlaying = function() { return !player.paused; }; pub.isPlaying = pIsPlaying; const pIsMuted = function() { return player.muted; }; pub.isMuted = pIsMuted; let lastMuteState = player.muted; player.addEventListener('volumechange', function() { if(lastMuteState !== player.muted) { lastMuteState = player.muted; watchers.call('mute', lastMuteState); } else watchers.call('volume', player.volume); }); const pSetMuted = function(state) { player.muted = state; }; pub.setMuted = pSetMuted; const pGetVolume = function() { return player.volume; }; pub.getVolume = pGetVolume; const pSetVolume = function(volume) { player.volume = volume; }; pub.setVolume = pSetVolume; const pGetPlaybackRate = function() { return player.playbackRate; }; pub.getPlaybackRate = pGetPlaybackRate; player.addEventListener('ratechange', function() { watchers.call('rate', player.playbackRate); }); const pSetPlaybackRate = function(rate) { player.playbackRate = rate; }; pub.setPlaybackRate = pSetPlaybackRate; window.addEventListener('durationchange', function() { watchers.call('duration', player.duration); }); const pGetDuration = function() { return player.duration; }; pub.getDuration = pGetDuration; window.addEventListener('timeupdate', function() { watchers.call('time', player.currentTime); }); const pGetTime = function() { return player.currentTime; }; pub.getTime = pGetTime; const pSeek = function(time) { player.currentTime = time; }; pub.seek = pSeek; return pub; }; const MszVideoEmbedYouTube = function(metadata, options) { options = options || {}; const ytOrigin = 'https://www.youtube.com', playerId = 'yt-' + MszUniqueStr(8), shouldAutoplay = options.autoplay === undefined || options.autoplay; let embedUrl = 'https://www.youtube.com/embed/' + metadata.youtube_video_id + '?enablejsapi=1'; embedUrl += '&origin=' + encodeURIComponent(location.origin); if(metadata.youtube_start_time) embedUrl += '&t=' + encodeURIComponent(metadata.youtube_start_time); if(metadata.youtube_playlist) { embedUrl += '&list=' + encodeURIComponent(metadata.youtube_playlist); if(metadata.youtube_playlist_index) embedUrl += '&index=' + encodeURIComponent(metadata.youtube_playlist_index); } let presetPlaybackRates = undefined, isMuted = undefined, volume = undefined, playbackRate = undefined, duration = undefined, currentTime = undefined, isPlaying = undefined; const watchers = new MszWatcherCollection; watchers.define(MszVideoEmbedPlayerEvents()); const player = $e({ tag: 'iframe', attrs: { frameborder: 0, allow: 'accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture', allowfullscreen: 'allowfullscreen', src: embedUrl, }, }); const pub = { getElement: function() { return player; }, appendTo: function(target) { target.appendChild(player); }, insertBefore: function(ref) { $ib(ref, player); }, nuke: function() { $r(player); }, replaceElement(target) { $ib(target, player); $r(target); }, getType: function() { return 'youtube'; }, getWidth: function() { return 560; }, getHeight: function() { return 315; }, getPlayerId: function() { return playerId; }, }; pub.watch = (name, handler) => watchers.watch(name, handler); pub.unwatch = (name, handler) => watchers.unwatch(name, handler); const postMessage = function(data) { player.contentWindow.postMessage(JSON.stringify(data), ytOrigin); }; const postCommand = function(name, args) { postMessage({ id: playerId, event: 'command', func: name, args: args || [], }); }; const cmdPlay = function() { postCommand('playVideo'); }; const cmdPause = function() { postCommand('pauseVideo'); }; const cmdStop = function() { postCommand('stopVideo'); }; const cmdMute = function() { postCommand('mute'); }; const cmdUnMute = function() { postCommand('unMute'); }; const cmdSetVolume = function(volume) { postCommand('setVolume', [volume * 100]); }; const cmdSetPlaybackRate = function(rate) { postCommand('setPlaybackRate', [rate]); }; const cmdSeekTo = function(timecode) { postCommand('seekTo', [timecode, true]); }; const cmdMuteState = function(state) { if(state) cmdMute(); else cmdUnMute(); }; const varIsPlaying = function() { return isPlaying; }; const varIsMuted = function() { return isMuted; }; const varVolume = function() { return volume; }; const varRate = function() { return playbackRate; }; const varDuration = function() { return duration; }; const varTime = function() { return currentTime; }; let lastPlayerState = undefined, lastPlayerStateEvent = undefined; const handlePlayerState = function(state) { let eventName = undefined; if(state === 1) { isPlaying = true; eventName = 'play'; } else { isPlaying = false; if(state === 2) eventName = 'pause'; else if(lastPlayerState === -1 && state === 5) eventName = 'stop'; } lastPlayerState = state; if(eventName !== undefined && eventName !== lastPlayerStateEvent) { lastPlayerStateEvent = eventName; watchers.call(eventName); } }; const handleMuted = function(muted) { isMuted = muted; watchers.call('mute', isMuted); }; const handleVolume = function(value) { volume = value / 100; watchers.call('volume', volume); }; const handleRate = function(rate) { playbackRate = rate; watchers.call('rate', playbackRate); }; const handleDuration = function(time) { duration = time; watchers.call('duration', duration); }; const handleTime = function(time) { currentTime = time; watchers.call('time', currentTime); }; const handlePresetRates = function(rates) { presetPlaybackRates = rates; }; const infoHandlers = { 'playerState': handlePlayerState, 'muted': handleMuted, 'volume': handleVolume, 'playbackRate': handleRate, 'duration': handleDuration, 'currentTime': handleTime, 'availablePlaybackRates': handlePresetRates, }; const processInfo = function(info) { for(const name in infoHandlers) if(name in info && info[name] !== undefined) infoHandlers[name](info[name]); }; window.addEventListener('message', function(ev) { if(ev.origin !== ytOrigin || typeof ev.data !== 'string') return; const data = JSON.parse(ev.data); if(!data || data.id !== playerId) return; if(data.event === 'initialDelivery') { if(data.info !== undefined) processInfo(data.info); } else if(data.event === 'onReady') { if(shouldAutoplay) cmdPlay(); } else if(data.event === 'infoDelivery') { if(data.info !== undefined) processInfo(data.info); } }); player.addEventListener('load', function(ev) { postMessage({ id: playerId, event: 'listening', }); }); pub.play = cmdPlay; pub.pause = cmdPause; pub.stop = cmdStop; pub.isPlaying = varIsPlaying; pub.isMuted = varIsMuted; pub.setMuted = cmdMuteState; pub.getVolume = varVolume; pub.setVolume = cmdSetVolume; pub.getRate = varRate; pub.setRate = cmdSetPlaybackRate; pub.getDuration = varDuration; pub.getTime = varTime; pub.seek = cmdSeekTo; return pub; }; const MszVideoEmbedNicoNico = function(metadata, options) { options = options || {}; const nndOrigin = 'https://embed.nicovideo.jp', playerId = 'nnd-' + MszUniqueStr(8), shouldAutoplay = options.autoplay === undefined || options.autoplay; let embedUrl = 'https://embed.nicovideo.jp/watch/' + metadata.nicovideo_video_id + '?jsapi=1&playerId=' + playerId; if(metadata.nicovideo_start_time) embedUrl += '&from=' + encodeURIComponent(metadata.nicovideo_start_time); let isMuted = undefined, volume = undefined, duration = undefined, currentTime = undefined, isPlaying = false; const watchers = new MszWatcherCollection; watchers.define(MszVideoEmbedPlayerEvents()); const player = $e({ tag: 'iframe', attrs: { frameborder: 0, allow: 'autoplay', allowfullscreen: 'allowfullscreen', src: embedUrl, }, }); const pub = { getElement: function() { return player; }, appendTo: function(target) { target.appendChild(player); }, insertBefore: function(ref) { $ib(ref, player); }, nuke: function() { $r(player); }, replaceElement(target) { $ib(target, player); $r(target); }, getType: function() { return 'nicovideo'; }, getWidth: function() { return 640; }, getHeight: function() { return 360; }, getPlayerId: function() { return playerId; }, }; pub.watch = (name, handler) => watchers.watch(name, handler); pub.unwatch = (name, handler) => watchers.unwatch(name, handler); const postMessage = function(name, data) { if(name === undefined) throw 'name must be specified'; player.contentWindow.postMessage({ playerId: playerId, sourceConnectorType: 1, eventName: name, data: data, }, nndOrigin); }; const cmdPlay = function() { postMessage('play'); }; const cmdPause = function() { postMessage('pause'); }; const cmdMute = function(state) { postMessage('mute', { mute: state }); }; const cmdSeek = function(time) { postMessage('seek', { time: time }); }; const cmdVolumeChange = function(volume) { postMessage('volumeChange', { volume: volume }); }; let stopCalled = false; const cmdStop = function() { stopCalled = true; cmdPause(); cmdSeek(0); }; const varIsPlaying = function() { return isPlaying; }; const varIsMuted = function() { return isMuted; }; const varVolume = function() { return volume; }; const varDuration = function() { return duration; }; const varTime = function() { return currentTime; }; let lastPlayerStateEvent = undefined; const handlePlayerStatus = function(status) { let eventName = undefined; if(status === 2) { isPlaying = true; eventName = 'play'; } else { isPlaying = false; if(status === 4 || stopCalled) { eventName = 'stop'; stopCalled = false; } else if(status === 3) eventName = 'pause'; } if(eventName !== undefined && eventName !== lastPlayerStateEvent) { lastPlayerStateEvent = eventName; watchers.call(eventName); } }; const handleMuted = function(muted) { isMuted = muted; watchers.call('mute', isMuted); }; const handleVolume = function(value) { volume = value; watchers.call('volume', volume); }; const handleDuration = function(time) { duration = time / 1000; watchers.call('duration', duration); }; const handleTime = function(time) { currentTime = time / 1000; watchers.call('time', currentTime); }; const metadataHanders = { 'muted': handleMuted, 'volume': handleVolume, 'duration': handleDuration, 'currentTime': handleTime, }; const statusHandlers = { 'playerStatus': handlePlayerStatus, }; const processData = function(handlers, info) { for(const name in handlers) if(name in info && info[name] !== undefined) handlers[name](info[name]); }; window.addEventListener('message', function(ev) { if(ev.origin !== nndOrigin || ev.data.playerId !== playerId) return; if(ev.data.eventName === 'loadComplete') { if(shouldAutoplay) cmdPlay(); } else if(ev.data.eventName === 'playerMetadataChange') { if(ev.data.data !== undefined) processData(metadataHanders, ev.data.data); } else if(ev.data.eventName === 'statusChange') { if(ev.data.data !== undefined) processData(statusHandlers, ev.data.data); } }); pub.play = cmdPlay; pub.pause = cmdPause; pub.stop = cmdStop; pub.isPlaying = varIsPlaying; pub.isMuted = varIsMuted; pub.setMuted = cmdMute; pub.getVolume = varVolume; pub.setVolume = cmdVolumeChange; pub.getDuration = varDuration; pub.getTime = varTime; pub.seek = cmdSeek; return pub; }; const MszVideoEmbedPlaceholder = function(metadata, options) { options = options || {}; if(typeof options.player !== 'function' && typeof options.onclick !== 'function') throw 'Neither a player nor an onclick handler were provided.'; const shouldAutoSize = options.autosize === undefined || options.autosize; const infoChildren = []; infoChildren.push({ tag: 'h1', attrs: { className: 'embedph-info-title', }, child: metadata.title, }); if(metadata.description) { let firstLine = metadata.description.split("\n")[0].trim(); if(firstLine.length > 300) firstLine = firstLine.substring(0, 300).trim() + '...'; infoChildren.push({ tag: 'div', attrs: { className: 'embedph-info-desc', }, child: firstLine, }); } infoChildren.push({ tag: 'div', attrs: { className: 'embedph-info-site', }, child: metadata.site_name, }); const style = []; if(typeof metadata.color !== 'undefined') style.push('--embedph-colour: ' + metadata.color); if(!shouldAutoSize) { const size = MszVideoConstrainSize( options.width || metadata.width || 200, options.height || metadata.height || 200, options.maxWidth, options.maxHeight ); style.push('width: ' + size[0].toString() + 'px'); style.push('height: ' + size[1].toString() + 'px'); } const pub = {}; const elem = $e({ attrs: { className: ('embedph embedph-' + (options.type || 'external')), style: style.join(';'), }, child: [ { attrs: { className: 'embedph-bg', }, child: { tag: 'img', attrs: { src: metadata.image, }, }, }, { attrs: { className: 'embedph-fg', }, child: [ { attrs: { className: 'embedph-info', }, child: { attrs: { className: 'embedph-info-wrap', }, child: [ { attrs: { className: 'embedph-info-bar', }, }, { attrs: { className: 'embedph-info-body', }, child: infoChildren, } ], }, }, { attrs: { className: 'embedph-play', onclick: function(ev) { if(ev.target.tagName.toLowerCase() === 'a') return; if(typeof options.onclick === 'function') { options.onclick(ev); return; } const player = new options.player(metadata, options); let frameOrPlayer = player; if(typeof options.frame === 'function') frameOrPlayer = new options.frame(player, options); const embed = new MszVideoEmbed(frameOrPlayer); if(options.autoembed === undefined || options.autoembed) embed.replaceElement(elem); if(typeof options.onembed === 'function') options.onembed(embed); }, }, child: [ { attrs: { className: 'embedph-play-internal', }, child: { tag: 'i', attrs: { className: 'fas fa-play fa-4x fa-fw', }, }, }, { tag: 'a', attrs: { className: 'embedph-play-external', href: metadata.url, target: '_blank', rel: 'noopener', }, child: ('or watch on ' + metadata.site_name + '?'), } ], }, ], }, ], }); pub.getElement = function() { return elem; }; pub.appendTo = function(target) { target.appendChild(elem); }; pub.insertBefore = function(ref) { $ib(ref, elem); }; pub.nuke = function() { $r(elem); }; pub.replaceElement = function(target) { $ib(target, elem); $r(target); }; return pub; };