Add Niconico support

This commit is contained in:
Xaekai 2022-01-31 16:45:44 -08:00
parent 094c9a7c4e
commit dd051098bc
8 changed files with 325 additions and 1 deletions

View File

@ -8,6 +8,7 @@ var order = [
'base.coffee', 'base.coffee',
'dailymotion.coffee', 'dailymotion.coffee',
'niconico.coffee',
'peertube.coffee', 'peertube.coffee',
'soundcloud.coffee', 'soundcloud.coffee',
'twitch.coffee', 'twitch.coffee',

66
player/niconico.coffee Normal file
View File

@ -0,0 +1,66 @@
window.NicoPlayer = class NicoPlayer extends Player
constructor: (data) ->
if not (this instanceof NicoPlayer)
return new NicoPlayer(data)
@load(data)
load: (data) ->
@setMediaProperties(data)
waitUntilDefined(window, 'NicovideoEmbed', =>
@nico = new NicovideoEmbed({ playerId: 'ytapiplayer', videoId: data.id })
removeOld($(@nico.iframe))
@nico.on('ended', =>
if CLIENT.leader
socket.emit('playNext')
)
@nico.on('pause', =>
@paused = true
if CLIENT.leader
sendVideoUpdate()
)
@nico.on('play', =>
@paused = false
if CLIENT.leader
sendVideoUpdate()
)
@nico.on('ready', =>
@play()
@setVolume(VOLUME)
)
)
play: ->
@paused = false
if @nico
@nico.play()
pause: ->
@paused = true
if @nico
@nico.pause()
seekTo: (time) ->
if @nico
@nico.seek(time * 1000)
setVolume: (volume) ->
if @nico
@nico.volumeChange(volume)
getTime: (cb) ->
if @nico
cb(parseFloat(@nico.state.currentTime / 1000))
else
cb(0)
getVolume: (cb) ->
if @nico
cb(parseFloat(@nico.state.volume))
else
cb(VOLUME)

View File

@ -18,6 +18,7 @@ TYPE_MAP =
bc: IframeChild bc: IframeChild
bn: IframeChild bn: IframeChild
od: OdyseePlayer od: OdyseePlayer
nv: NicoPlayer
window.loadMediaPlayer = (data) -> window.loadMediaPlayer = (data) ->
try try
@ -109,7 +110,8 @@ window.removeOld = (replace) ->
$('#soundcloud-volume-holder').remove() $('#soundcloud-volume-holder').remove()
replace ?= $('<div/>').addClass('embed-responsive-item') replace ?= $('<div/>').addClass('embed-responsive-item')
old = $('#ytapiplayer') old = $('#ytapiplayer')
old.attr('id', 'ytapiplayer-old')
replace.attr('id', 'ytapiplayer')
replace.insertBefore(old) replace.insertBefore(old)
old.remove() old.remove()
replace.attr('id', 'ytapiplayer')
return replace return replace

View File

@ -10,6 +10,7 @@ const Odysee = require("@cytube/mediaquery/lib/provider/odysee");
const PeerTube = require("@cytube/mediaquery/lib/provider/peertube"); const PeerTube = require("@cytube/mediaquery/lib/provider/peertube");
const BitChute = require("@cytube/mediaquery/lib/provider/bitchute"); const BitChute = require("@cytube/mediaquery/lib/provider/bitchute");
const BandCamp = require("@cytube/mediaquery/lib/provider/bandcamp"); const BandCamp = require("@cytube/mediaquery/lib/provider/bandcamp");
const Nicovideo = require("@cytube/mediaquery/lib/provider/nicovideo");
const Streamable = require("@cytube/mediaquery/lib/provider/streamable"); const Streamable = require("@cytube/mediaquery/lib/provider/streamable");
const TwitchVOD = require("@cytube/mediaquery/lib/provider/twitch-vod"); const TwitchVOD = require("@cytube/mediaquery/lib/provider/twitch-vod");
const TwitchClip = require("@cytube/mediaquery/lib/provider/twitch-clip"); const TwitchClip = require("@cytube/mediaquery/lib/provider/twitch-clip");
@ -416,6 +417,16 @@ var Getters = {
}).catch(error => { }).catch(error => {
callback(error.message || error); callback(error.message || error);
}); });
},
/* Niconico */
nv: function (id, callback) {
Nicovideo.lookup(id).then(video => {
video = new Media(video.id, video.title, video.duration, "nv", video.meta);
callback(null, video);
}).catch(error => {
callback(error.message || error);
});
} }
}; };

View File

@ -216,6 +216,8 @@
case "od": case "od":
const [user,video] = id.split(';'); const [user,video] = id.split(';');
return `https://odysee.com/@${user}/${video}`; return `https://odysee.com/@${user}/${video}`;
case "nv":
return `https://www.nicovideo.jp/watch/${id}`;
default: default:
return ""; return "";
} }

View File

@ -252,6 +252,7 @@ html(lang="en")
script(defer, src="https://www.youtube.com/iframe_api") script(defer, src="https://www.youtube.com/iframe_api")
script(defer, src="https://api.dmcdn.net/all.js") script(defer, src="https://api.dmcdn.net/all.js")
script(defer, src="https://player.vimeo.com/api/player.js") script(defer, src="https://player.vimeo.com/api/player.js")
script(defer, src="/js/niconico.js")
script(defer, src="/js/peertube.js") script(defer, src="/js/peertube.js")
script(defer, src="/js/sc.js") script(defer, src="/js/sc.js")
script(defer, src="/js/video.js") script(defer, src="/js/video.js")

234
www/js/niconico.js Normal file
View File

@ -0,0 +1,234 @@
/*
* Niconico iframe embed api
* Written by Xaekai
* Copyright (c) 2022 Radiant Feather; Licensed AGPLv3
*
*/
class NicovideoEmbed {
static origin = 'https://embed.nicovideo.jp';
static methods = [
'loadComplete',
'mute',
'pause',
'play',
'seek',
'volumeChange',
];
static frames = [
'error',
'loadComplete',
'playerMetadataChange',
'playerStatusChange',
'seekStatusChange',
'statusChange',
//'player-error:video:play',
//'player-error:video:seek',
];
static events = [
'ended',
'error',
'muted',
'pause',
'play',
'progress',
'ready',
'timeupdate',
'unmuted',
'volumechange',
];
constructor(options) {
this.handlers = Object.fromEntries(NicovideoEmbed.frames.map(key => [key,[]]));
this.listeners = Object.fromEntries(NicovideoEmbed.events.map(key => [key,[]]));
this.state = ({
ready: false,
playerStatus: 1,
currentTime: 0.0, // ms
muted: false,
volume: 0.99,
maximumBuffered: 0,
});
this.setupHandlers();
this.scaffold(options);
}
scaffold({ iframe = null, playerId = 1, videoId = null }){
this.playerId = playerId;
this.messageListener();
if(iframe === null){
if(videoId === null){
throw new Error('You must provide either an existing iframe or a videoId');
}
const iframe = this.iframe = document.createElement('iframe');
const source = new URL(`${NicovideoEmbed.origin}/watch/${videoId}`);
source.search = new URLSearchParams({
jsapi: 1,
playerId
});
iframe.setAttribute('src', source);
iframe.setAttribute('id', playerId);
iframe.setAttribute('allow', 'autoplay; fullscreen');
iframe.addEventListener('load', ()=>{
this.observe();
})
} else {
this.iframe = iframe;
this.observe();
}
}
setupHandlers() {
this.handlers.loadComplete.push((data) => {
this.emit('ready');
this.state.ready = true;
Object.assign(this, data);
});
this.handlers.error.push((data) => {
this.emit('error', data);
});
this.handlers.playerStatusChange.push((data) => {
let event;
switch (data.playerStatus) {
case 1: /* Buffering */ return;
case 2: event = 'play'; break;
case 3: event = 'pause'; break;
case 4: event = 'ended'; break;
}
this.state.playerStatus = data.playerStatus;
this.emit(event);
});
this.handlers.playerMetadataChange.push(({ currentTime, volume, muted, maximumBuffered }) => {
const self = this.state;
if (currentTime !== self.currentTime) {
self.currentTime = currentTime;
this.emit('timeupdate', currentTime);
}
if (muted !== self.muted) {
self.muted = muted;
this.emit(muted ? 'muted' : 'unmuted');
}
if (volume !== self.volume) {
self.volume = volume;
this.emit('volumechange', volume);
}
if (maximumBuffered !== self.maximumBuffered) {
self.maximumBuffered = maximumBuffered;
this.emit('progress', maximumBuffered);
}
});
this.handlers.seekStatusChange.push((data) => {
//
});
this.handlers.statusChange.push((data) => {
//
});
}
messageListener() {
const dispatcher = (event) => {
if (event.origin === NicovideoEmbed.origin && event.data.playerId === this.playerId) {
const { data } = event.data;
this.dispatch(event.data.eventName, data);
}
}
window.addEventListener('message', dispatcher);
/* Clean up */
this.observer = new MutationObserver((alterations) => {
alterations.forEach((change) => {
change.removedNodes.forEach((deletion) => {
if(deletion.nodeName === 'IFRAME') {
window.removeEventListener('message', dispatcher)
this.observer.disconnect();
}
});
});
});
}
observe(){
this.state.receptive = true;
this.observer.observe(this.iframe.parentElement, { subtree: true, childList: true });
}
dispatch(frame, data = null){
if(!NicovideoEmbed.frames.includes(frame)){
console.error(JSON.stringify(data, undefined, 4));
throw new Error(`NicovideoEmbed ${frame}`);
}
[...this.handlers[frame]].forEach(handler => {
handler.call(this, data);
});
}
emit(event, data = null){
[...this.listeners[event]].forEach(listener => {
listener.call(this, data);
});
if(event === 'ready'){
this.listeners.ready.length = 0;
}
}
postMessage(request) {
if(!this.state.receptive){
setTimeout(() => { this.postMessage(request) }, 1000 / 24);
return;
}
const message = Object.assign({
sourceConnectorType: 1,
playerId: this.playerId
}, request);
this.iframe.contentWindow.postMessage(message, NicovideoEmbed.origin);
}
on(event, listener){
if(!NicovideoEmbed.events.includes(event)){
throw new Error('Unrecognized event name');
}
if(event === 'ready'){
if(this.state.ready){
listener();
return this;
} else {
setTimeout(() => { this.loadComplete() }, 1000 / 60);
}
}
this.listeners[event].push(listener);
return this;
}
mute(state){
this.postMessage({ eventName: 'pause', data: { mute: state } });
}
pause(){
this.postMessage({ eventName: 'pause' });
}
play(){
this.postMessage({ eventName: 'play' });
}
loadComplete(){
this.postMessage({ eventName: 'loadComplete' });
}
seek(ms){
this.postMessage({ eventName: 'seek', data: { time: ms } });
}
volumeChange(volume){
this.postMessage({ eventName: 'pause', data: { volume } });
}
}
window.NicovideoEmbed = NicovideoEmbed;

View File

@ -67,6 +67,8 @@ function formatURL(data) {
case "od": case "od":
const [user,video] = data.id.split(';'); const [user,video] = data.id.split(';');
return `https://odysee.com/@${user}/${video}`; return `https://odysee.com/@${user}/${video}`;
case "nv":
return `https://www.nicovideo.jp/watch/${data.id}`;
default: default:
return "#"; return "#";
} }
@ -1406,6 +1408,11 @@ function parseMediaLink(url) {
return { type: 'bc', id: `${data.pathname.slice(7).split('/').shift()}` } return { type: 'bc', id: `${data.pathname.slice(7).split('/').shift()}` }
} }
case 'nicovideo.jp':
if(data.pathname.startsWith('/watch/')){
return { type: 'nv', id: `${data.pathname.slice(7).split('/').shift()}` }
}
case 'odysee.com': case 'odysee.com':
const format = new RegExp('/@(?<user>[^:]+)(?::\\w)?/(?<video>[^:]+)'); const format = new RegExp('/@(?<user>[^:]+)(?::\\w)?/(?<video>[^:]+)');
if(format.test(data.pathname)){ if(format.test(data.pathname)){