Compare commits
63 commits
renaissanc
...
postgreSQL
Author | SHA1 | Date | |
---|---|---|---|
db6253cd04 | |||
secretspecter | 4846b22cd1 | ||
secretspecter | 0b806cec92 | ||
227244e2d0 | |||
6f47ed42db | |||
98bfb6736e | |||
2c541448a2 | |||
21d7f16413 | |||
87198bd4e7 | |||
986207b46b | |||
ed410fdebe | |||
1a9d920884 | |||
c78ef333da | |||
fad1da7ab4 | |||
d37e69e1a6 | |||
1e2dcee4fa | |||
6ec2f3d491 | |||
306e3adde8 | |||
99740a3673 | |||
913348d46e | |||
ae5dbf5f48 | |||
8338fe2f25 | |||
7921f41174 | |||
50e2692896 | |||
9e0f7b8efa | |||
fd9586e0da | |||
f185e6c3ea | |||
2cf26cdc4c | |||
008c24f892 | |||
a398e3a6fa | |||
aa04f0d034 | |||
e7f0aa98be | |||
0f9d778a27 | |||
119b6a62b8 | |||
9d00d9666d | |||
f6ba5b71e8 | |||
9b05e2eb8c | |||
911558760f | |||
45217ccad8 | |||
aeb5de85b6 | |||
53911ab9f0 | |||
a2c4ea5036 | |||
517058bef3 | |||
1790d5b569 | |||
97b8d1b4b7 | |||
25ddc336e0 | |||
498272b128 | |||
26f6611ca8 | |||
6b831bc367 | |||
ffd01fe30b | |||
8774dc89e7 | |||
16f183c117 | |||
ba80c1591d | |||
4fada9a8d2 | |||
7441892235 | |||
f929758bfd | |||
500f295506 | |||
de1f37735b | |||
9f9bbfa022 | |||
d516c5ebfc | |||
3668c1b3da | |||
0e3307b9f4 | |||
3ea16944d2 |
6
.editorconfig
Normal file
6
.editorconfig
Normal file
|
@ -0,0 +1,6 @@
|
|||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
indent_size = 4
|
||||
indent_style = space
|
45
.eslintrc.js
Normal file
45
.eslintrc.js
Normal file
|
@ -0,0 +1,45 @@
|
|||
/* ESLint Config */
|
||||
module.exports = {
|
||||
env: {
|
||||
'es2017': true,
|
||||
// others envs defined by cascading .eslintrc files
|
||||
},
|
||||
extends: 'eslint:recommended',
|
||||
parser: '@babel/eslint-parser',
|
||||
parserOptions: {
|
||||
'sourceType': 'module',
|
||||
},
|
||||
rules: {
|
||||
'brace-style': ['error','1tbs',{ 'allowSingleLine': true }],
|
||||
'indent': [
|
||||
'off', // temporary... a lot of stuff needs to be reformatted | 2020-08-21: I guess it's not so temporary...
|
||||
4,
|
||||
{ 'SwitchCase': 1 }
|
||||
],
|
||||
'linebreak-style': ['error','unix'],
|
||||
'no-control-regex': ['off'],
|
||||
'no-prototype-builtins': ['off'], // should consider cleaning up the code and turning this back on at some point
|
||||
'no-trailing-spaces': ['error'],
|
||||
'no-unused-vars': [
|
||||
'error', {
|
||||
'argsIgnorePattern': '^_',
|
||||
'varsIgnorePattern': '^_|^Promise$'
|
||||
}
|
||||
],
|
||||
'semi': ['error','always'],
|
||||
'quotes': ['off'] // Old code uses double quotes, new code uses single / template
|
||||
},
|
||||
ignorePatterns: [
|
||||
// These are not ours
|
||||
'www/js/dash.all.min.js',
|
||||
'www/js/jquery-1.12.4.min.js',
|
||||
'www/js/jquery-ui.js',
|
||||
'www/js/peertube.js',
|
||||
'www/js/playerjs-0.0.12.js',
|
||||
'www/js/sc.js',
|
||||
'www/js/video.js',
|
||||
'www/js/videojs-contrib-hls.min.js',
|
||||
'www/js/videojs-dash.js',
|
||||
'www/js/videojs-resolution-switcher.js',
|
||||
],
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
env:
|
||||
es6: true
|
||||
node: true
|
||||
extends: 'eslint:recommended'
|
||||
parser: '@babel/eslint-parser'
|
||||
parserOptions:
|
||||
sourceType: module
|
||||
ecmaVersion: 2017 # For async/await
|
||||
rules:
|
||||
brace-style:
|
||||
- error
|
||||
- 1tbs
|
||||
- allowSingleLine: true
|
||||
indent:
|
||||
- off # temporary... a lot of stuff needs to be reformatted | 2020-08-21: I guess it's not so temporary...
|
||||
- 4
|
||||
- SwitchCase: 1
|
||||
linebreak-style:
|
||||
- error
|
||||
- unix
|
||||
no-control-regex:
|
||||
- off
|
||||
no-prototype-builtins:
|
||||
- off # should consider cleaning up the code and turning this back on at some point
|
||||
no-trailing-spaces:
|
||||
- error
|
||||
no-unused-vars:
|
||||
- error
|
||||
- argsIgnorePattern: ^_
|
||||
varsIgnorePattern: ^_|^Promise$
|
||||
semi:
|
||||
- error
|
||||
- always
|
||||
quotes:
|
||||
- off # Old code uses double quotes, new code uses single / template
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -18,3 +18,4 @@ www/js/cytube-google-drive.user.js
|
|||
www/js/cytube-google-drive.meta.js
|
||||
www/js/player.js
|
||||
tor-exit-list.json
|
||||
*.patch
|
||||
|
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
The MIT License (MIT)
|
||||
Copyright (c) 2013-2021 Calvin Montgomery and contributors
|
||||
Copyright (c) 2013-2022 Calvin Montgomery and contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
|
|
30
NEWS.md
30
NEWS.md
|
@ -1,3 +1,33 @@
|
|||
2022-09-21
|
||||
==========
|
||||
|
||||
**Upgrade intervention required**
|
||||
|
||||
This release adds a feature to ban channels, replacing the earlier (hastily
|
||||
added) configuration-based `channel-blacklist`. If you have any entries in
|
||||
`channel-blacklist` in your `config.yaml`, you will need to migrate them to the
|
||||
new bans table by using a command after upgrading (the ACP web interface hasn't
|
||||
been updated for this feature):
|
||||
|
||||
./bin/admin.js ban-channel <channel-name> <external-reason> <internal-reason>
|
||||
|
||||
The external reason will be displayed when users attempt to join the banned
|
||||
channel, while the internal reason is only displayed when using the
|
||||
`show-channel-ban` command.
|
||||
|
||||
You can later use `unban-channel` to remove a ban. The owner of the banned
|
||||
channel can still delete it, but the banned state will persist, so the channel
|
||||
cannot be re-registered later.
|
||||
|
||||
2022-08-28
|
||||
==========
|
||||
|
||||
This release integrates Xaekai's added support for Bandcamp, BitChute, Odysee,
|
||||
and Nicovideo playback support into the main repository. The updated support
|
||||
for custom fonts and audio tracks in custom media manifests is also included,
|
||||
but does not work out of the box -- it requires a separate channel script; this
|
||||
may be addressed in the future.
|
||||
|
||||
2021-08-14
|
||||
==========
|
||||
|
||||
|
|
176
bin/admin.js
Executable file
176
bin/admin.js
Executable file
|
@ -0,0 +1,176 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const Config = require('../lib/config');
|
||||
Config.load('config.yaml');
|
||||
|
||||
if (!Config.get('service-socket.enabled')){
|
||||
console.error('The Service Socket is not enabled.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const net = require('net');
|
||||
const path = require('path');
|
||||
const readline = require('node:readline/promises');
|
||||
|
||||
const socketPath = path.resolve(__dirname, '..', Config.get('service-socket.socket'));
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
async function doCommand(params) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const client = net.createConnection(socketPath);
|
||||
|
||||
client.on('connect', () => {
|
||||
client.write(JSON.stringify(params) + '\n');
|
||||
});
|
||||
|
||||
client.on('data', data => {
|
||||
client.end();
|
||||
resolve(JSON.parse(data));
|
||||
});
|
||||
|
||||
client.on('error', error => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
let commands = [
|
||||
{
|
||||
command: 'ban-channel',
|
||||
handler: async args => {
|
||||
if (args.length !== 3) {
|
||||
console.log('Usage: ban-channel <name> <externalReason> <internalReason>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let [name, externalReason, internalReason] = args;
|
||||
let answer = await rl.question(`Ban ${name} with external reason "${externalReason}" and internal reason "${internalReason}"? `);
|
||||
|
||||
if (!/^[yY]$/.test(answer)) {
|
||||
console.log('Aborted.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let res = await doCommand({
|
||||
command: 'ban-channel',
|
||||
name,
|
||||
externalReason,
|
||||
internalReason
|
||||
});
|
||||
|
||||
switch (res.status) {
|
||||
case 'error':
|
||||
console.log('Error:', res.error);
|
||||
process.exit(1);
|
||||
break;
|
||||
case 'success':
|
||||
console.log('Ban succeeded.');
|
||||
process.exit(0);
|
||||
break;
|
||||
default:
|
||||
console.log(`Unknown result: ${res.status}`);
|
||||
process.exit(1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
command: 'unban-channel',
|
||||
handler: async args => {
|
||||
if (args.length !== 1) {
|
||||
console.log('Usage: unban-channel <name>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let [name] = args;
|
||||
let answer = await rl.question(`Unban ${name}? `);
|
||||
|
||||
if (!/^[yY]$/.test(answer)) {
|
||||
console.log('Aborted.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let res = await doCommand({
|
||||
command: 'unban-channel',
|
||||
name
|
||||
});
|
||||
|
||||
switch (res.status) {
|
||||
case 'error':
|
||||
console.log('Error:', res.error);
|
||||
process.exit(1);
|
||||
break;
|
||||
case 'success':
|
||||
console.log('Unban succeeded.');
|
||||
process.exit(0);
|
||||
break;
|
||||
default:
|
||||
console.log(`Unknown result: ${res.status}`);
|
||||
process.exit(1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
command: 'show-banned-channel',
|
||||
handler: async args => {
|
||||
if (args.length !== 1) {
|
||||
console.log('Usage: show-banned-channel <name>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let [name] = args;
|
||||
|
||||
let res = await doCommand({
|
||||
command: 'show-banned-channel',
|
||||
name
|
||||
});
|
||||
|
||||
switch (res.status) {
|
||||
case 'error':
|
||||
console.log('Error:', res.error);
|
||||
process.exit(1);
|
||||
break;
|
||||
case 'success':
|
||||
if (res.ban != null) {
|
||||
console.log(`Channel: ${name}`);
|
||||
console.log(`Ban issued: ${res.ban.createdAt}`);
|
||||
console.log(`Banned by: ${res.ban.bannedBy}`);
|
||||
console.log(`External reason:\n${res.ban.externalReason}`);
|
||||
console.log(`Internal reason:\n${res.ban.internalReason}`);
|
||||
} else {
|
||||
console.log(`Channel ${name} is not banned.`);
|
||||
}
|
||||
process.exit(0);
|
||||
break;
|
||||
default:
|
||||
console.log(`Unknown result: ${res.status}`);
|
||||
process.exit(1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
let found = false;
|
||||
commands.forEach(cmd => {
|
||||
if (cmd.command === process.argv[2]) {
|
||||
found = true;
|
||||
cmd.handler(process.argv.slice(3)).then(() => {
|
||||
process.exit(0);
|
||||
}).catch(error => {
|
||||
console.log('Error in command:', error.stack);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (!found) {
|
||||
console.log('Available commands:');
|
||||
commands.forEach(cmd => {
|
||||
console.log(` * ${cmd.command}`);
|
||||
});
|
||||
process.exit(1);
|
||||
}
|
|
@ -6,25 +6,35 @@ var path = require('path');
|
|||
|
||||
var order = [
|
||||
'base.coffee',
|
||||
|
||||
'dailymotion.coffee',
|
||||
'niconico.coffee',
|
||||
'peertube.coffee',
|
||||
'soundcloud.coffee',
|
||||
'twitch.coffee',
|
||||
'vimeo.coffee',
|
||||
'youtube.coffee',
|
||||
'dailymotion.coffee',
|
||||
'videojs.coffee',
|
||||
|
||||
// playerjs-based players
|
||||
'playerjs.coffee',
|
||||
'iframechild.coffee',
|
||||
'odysee.coffee',
|
||||
'streamable.coffee',
|
||||
'gdrive-player.coffee',
|
||||
'raw-file.coffee',
|
||||
'soundcloud.coffee',
|
||||
|
||||
// iframe embed-based players
|
||||
'embed.coffee',
|
||||
'twitch.coffee',
|
||||
'livestream.com.coffee',
|
||||
'custom-embed.coffee',
|
||||
'rtmp.coffee',
|
||||
'smashcast.coffee',
|
||||
'ustream.coffee',
|
||||
'imgur.coffee',
|
||||
'hls.coffee',
|
||||
'livestream.com.coffee',
|
||||
'twitchclip.coffee',
|
||||
|
||||
// video.js-based players
|
||||
'videojs.coffee',
|
||||
'gdrive-player.coffee',
|
||||
'hls.coffee',
|
||||
'raw-file.coffee',
|
||||
'rtmp.coffee',
|
||||
|
||||
// mediaUpdate handler
|
||||
'update.coffee'
|
||||
];
|
||||
|
||||
|
|
|
@ -128,6 +128,8 @@ max-channels-per-user: 5
|
|||
max-accounts-per-ip: 5
|
||||
# Minimum number of seconds between guest logins from the same IP
|
||||
guest-login-delay: 60
|
||||
# Maximum character length of a chat message, capped at 1000 characters
|
||||
max-chat-message-length: 320
|
||||
|
||||
# Allows you to customize the path divider. The /r/ in http://localhost/r/yourchannel
|
||||
# Acceptable characters are a-z A-Z 0-9 _ and -
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
CyTube Custom Content Metadata
|
||||
==============================
|
||||
|
||||
*Last updated: 2019-05-05*
|
||||
*Last updated: 2022-02-12*
|
||||
|
||||
## Purpose ##
|
||||
|
||||
|
@ -61,6 +61,8 @@ To add custom content, the user provides a JSON object with the following keys:
|
|||
playlist, but this functionality may be offered in the future.
|
||||
* `sources`: A nonempty list of playable sources for the content. The format
|
||||
is described below.
|
||||
* `audioTracks`: An optional list of audio tracks for using demuxed audio
|
||||
and providing multiple audio selections. The format is described below.
|
||||
* `textTracks`: An optional list of text tracks for subtitles or closed
|
||||
captioning. The format is described below.
|
||||
|
||||
|
@ -99,19 +101,46 @@ The following MIME types are accepted for the `contentType` field:
|
|||
RTMP streams are only supported through the existing `rt:` media
|
||||
type.
|
||||
* `audio/aac`
|
||||
* `audio/ogg`
|
||||
* `audio/mp4`
|
||||
* `audio/mpeg`
|
||||
* `audio/ogg`
|
||||
|
||||
Other audio or video formats, such as AVI, MKV, and FLAC, are not supported due
|
||||
to lack of common support across browsers for playing these formats. For more
|
||||
information, refer to
|
||||
[MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Supported_media_formats#Browser_compatibility).
|
||||
|
||||
### Audio Track Format ###
|
||||
|
||||
Each audio track entry is a JSON object with the following keys:
|
||||
|
||||
* `label`: A label for the audio track. This is displayed in the menu for the
|
||||
viewer to select a text track.
|
||||
* `language`: A two or three letter IETF BCP 47 subtype code indicating the
|
||||
language of the audio track.
|
||||
* `url`: A valid URL that browsers can use to retrieve the track. The URL
|
||||
must resolve to a publicly-routed IP address, and must use the `https:` scheme.
|
||||
* `contentType`: A string representing the MIME type of the track at `url`.
|
||||
Any type starting with `audio` from the list above is acceptable. However
|
||||
the usage of audio/aac is known to cause audio syncrhonization problems
|
||||
for some users. It is recommended to use an m4a file to wrap aac streams.
|
||||
|
||||
**Important note regarding audio tracks:**
|
||||
|
||||
Because of browsers trying to be too smart for their own good, you should
|
||||
include a silent audio stream in the video sources when using separate audio
|
||||
tracks. If you do not, the browser will automatically pause the video whenever
|
||||
the browser detects the page as not visible. There is no way to instruct it to
|
||||
not do so. You can readily accomplish the inclusion of a silent audio track
|
||||
with ffmpeg using the anullsrc filter like so:
|
||||
`ffmpeg -f lavfi -i anullsrc=channel_layout=stereo:sample_rate=48000 -i input.mp4 -c:v copy -c:a aac -shortest output.mp4`
|
||||
It is recommended to match the sample rate and codec you intend to use in your
|
||||
audioTracks in your silent track.
|
||||
|
||||
### Text Track Format ###
|
||||
|
||||
Each text track entry is a JSON object with the following keys:
|
||||
|
||||
|
||||
* `url`: A valid URL that browsers can use to retrieve the track. The URL
|
||||
must resolve to a publicly-routed IP address, and must the `https:` scheme.
|
||||
* `contentType`: A string representing the MIME type of the track at `url`.
|
||||
|
@ -177,3 +206,5 @@ non-exhaustive.
|
|||
to the browser.
|
||||
* The manifest includes source URLs or text track URLs with expiration times,
|
||||
session IDs, etc. in the URL querystring.
|
||||
* The manifest provides source URLs with non-silent audio as well as a list
|
||||
of audioTracks.
|
||||
|
|
|
@ -21,7 +21,6 @@ Setting | Description
|
|||
--------|------------
|
||||
Synchronize video playback | By default, CyTube attempts to synchronize the video so that everyone is watching at the same time. Some users with poor internet connections may wish to disable this in order to prevent excessive buffering due to constantly seeking forward.
|
||||
Synch threshold | The number of seconds your video is allowed to be ahead/behind before it is forcibly seeked to the correct position. Should be set to at least 2 seconds to avoid buffering problems and choppy playback.
|
||||
Set wmode=transparent | There's probably no reason to touch this unless you know what you're doing. Having a non-transparent wmode can cause modals to display behind the video player, but also can cause performance issues in some situations.
|
||||
Remove the video player | Automatically remove the video player on page load. Equivalent to manually clicking Layout->Remove Video every time you load a channel.
|
||||
Hide playlist buttons by default | Hides the control buttons from each video in the playlist, so that only the title is displayed. The control buttons can be shown by right clicking the video item in the playlist.
|
||||
Old style playlist buttons | Legacy feature introduced in CyTube 2.0 for those who preferred the old 1.0-style video control buttons.
|
||||
|
|
|
@ -110,6 +110,25 @@ describe('KickbanModule', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('rejects if the username is invalid', done => {
|
||||
mockUser.socket.emit = (frame, obj) => {
|
||||
if (frame === 'errorMsg') {
|
||||
assert.strictEqual(
|
||||
obj.msg,
|
||||
'Invalid username'
|
||||
);
|
||||
|
||||
done();
|
||||
}
|
||||
};
|
||||
|
||||
kickban.handleCmdBan(
|
||||
mockUser,
|
||||
'/ban test_user<>%$# because reasons',
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects if the user does not have ban permission', done => {
|
||||
mockUser.socket.emit = (frame, obj) => {
|
||||
if (frame === 'errorMsg') {
|
||||
|
|
109
integration_test/controller/banned-channels.js
Normal file
109
integration_test/controller/banned-channels.js
Normal file
|
@ -0,0 +1,109 @@
|
|||
const assert = require('assert');
|
||||
const { BannedChannelsController } = require('../../lib/controller/banned-channels');
|
||||
const dbChannels = require('../../lib/database/channels');
|
||||
const testDB = require('../testutil/db').testDB;
|
||||
const { EventEmitter } = require('events');
|
||||
|
||||
require('../../lib/database').init(testDB);
|
||||
|
||||
const testBan = {
|
||||
name: 'ban_test_1',
|
||||
externalReason: 'because I said so',
|
||||
internalReason: 'illegal content',
|
||||
bannedBy: 'admin'
|
||||
};
|
||||
|
||||
async function cleanupTestBan() {
|
||||
return dbChannels.removeBannedChannel(testBan.name);
|
||||
}
|
||||
|
||||
describe('BannedChannelsController', () => {
|
||||
let controller;
|
||||
let messages;
|
||||
|
||||
beforeEach(async () => {
|
||||
await cleanupTestBan();
|
||||
messages = new EventEmitter();
|
||||
controller = new BannedChannelsController(
|
||||
dbChannels,
|
||||
messages
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await cleanupTestBan();
|
||||
});
|
||||
|
||||
it('bans a channel', async () => {
|
||||
assert.strictEqual(await controller.getBannedChannel(testBan.name), null);
|
||||
|
||||
let received = null;
|
||||
messages.once('ChannelBanned', cb => {
|
||||
received = cb;
|
||||
});
|
||||
|
||||
await controller.banChannel(testBan);
|
||||
let info = await controller.getBannedChannel(testBan.name);
|
||||
for (let field of Object.keys(testBan)) {
|
||||
// Consider renaming parameter to avoid this branch
|
||||
if (field === 'name') {
|
||||
assert.strictEqual(info.channelName, testBan.name);
|
||||
} else {
|
||||
assert.strictEqual(info[field], testBan[field]);
|
||||
}
|
||||
}
|
||||
|
||||
assert.notEqual(received, null);
|
||||
assert.strictEqual(received.channel, testBan.name);
|
||||
assert.strictEqual(received.externalReason, testBan.externalReason);
|
||||
});
|
||||
|
||||
it('updates an existing ban', async () => {
|
||||
let received = [];
|
||||
messages.on('ChannelBanned', cb => {
|
||||
received.push(cb);
|
||||
});
|
||||
|
||||
await controller.banChannel(testBan);
|
||||
|
||||
let testBan2 = { ...testBan, externalReason: 'because of reasons' };
|
||||
await controller.banChannel(testBan2);
|
||||
|
||||
let info = await controller.getBannedChannel(testBan2.name);
|
||||
for (let field of Object.keys(testBan2)) {
|
||||
// Consider renaming parameter to avoid this branch
|
||||
if (field === 'name') {
|
||||
assert.strictEqual(info.channelName, testBan2.name);
|
||||
} else {
|
||||
assert.strictEqual(info[field], testBan2[field]);
|
||||
}
|
||||
}
|
||||
|
||||
assert.deepStrictEqual(received, [
|
||||
{
|
||||
channel: testBan.name,
|
||||
externalReason: testBan.externalReason
|
||||
},
|
||||
{
|
||||
channel: testBan2.name,
|
||||
externalReason: testBan2.externalReason
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('unbans a channel', async () => {
|
||||
let received = null;
|
||||
messages.once('ChannelUnbanned', cb => {
|
||||
received = cb;
|
||||
});
|
||||
|
||||
await controller.banChannel(testBan);
|
||||
await controller.unbanChannel(testBan.name, testBan.bannedBy);
|
||||
|
||||
let info = await controller.getBannedChannel(testBan.name);
|
||||
assert.strictEqual(info, null);
|
||||
|
||||
assert.notEqual(received, null);
|
||||
assert.strictEqual(received.channel, testBan.name);
|
||||
});
|
||||
});
|
8716
package-lock.json
generated
8716
package-lock.json
generated
File diff suppressed because it is too large
Load diff
18
package.json
18
package.json
|
@ -2,17 +2,17 @@
|
|||
"author": "Calvin Montgomery",
|
||||
"name": "CyTube",
|
||||
"description": "Online media synchronizer and chat",
|
||||
"version": "3.82.11",
|
||||
"version": "3.86.0",
|
||||
"repository": {
|
||||
"url": "http://github.com/calzoneman/sync"
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@calzoneman/jsli": "^2.0.1",
|
||||
"@cytube/mediaquery": "github:CyTube/mediaquery#4f803961d72a4fc7a1e09c0babaf8ea685013b1b",
|
||||
"@cytube/mediaquery": "github:CyTube/mediaquery#564d0c4615e80f72722b0f68ac81f837a4c5fc81",
|
||||
"bcrypt": "^5.0.1",
|
||||
"bluebird": "^3.7.2",
|
||||
"body-parser": "^1.19.0",
|
||||
"body-parser": "^1.20.1",
|
||||
"cheerio": "^1.0.0-rc.10",
|
||||
"clone": "^2.1.2",
|
||||
"compression": "^1.7.4",
|
||||
|
@ -20,21 +20,22 @@
|
|||
"create-error": "^0.3.1",
|
||||
"csrf": "^3.1.0",
|
||||
"cytubefilters": "github:calzoneman/cytubefilters#c67b2dab2dc5cc5ed11018819f71273d0f8a1bf5",
|
||||
"express": "^4.17.1",
|
||||
"express": "^4.18.2",
|
||||
"express-minify": "^1.0.0",
|
||||
"json-typecheck": "^0.1.3",
|
||||
"knex": "^0.95.2",
|
||||
"knex": "^2.4.0",
|
||||
"lodash": "^4.17.21",
|
||||
"morgan": "^1.10.0",
|
||||
"mysql": "^2.18.1",
|
||||
"nodemailer": "^6.6.1",
|
||||
"pg": "^8.11.3",
|
||||
"pg-native": "^3.0.1",
|
||||
"prom-client": "^13.1.0",
|
||||
"proxy-addr": "^2.0.6",
|
||||
"pug": "^3.0.2",
|
||||
"redis": "^3.1.1",
|
||||
"sanitize-html": "^2.7.0",
|
||||
"serve-static": "^1.14.1",
|
||||
"socket.io": "^4.5.0",
|
||||
"serve-static": "^1.15.0",
|
||||
"socket.io": "^4.5.4",
|
||||
"source-map-support": "^0.5.19",
|
||||
"toml": "^3.0.0",
|
||||
"uuid": "^8.3.2",
|
||||
|
@ -60,6 +61,7 @@
|
|||
"babel-plugin-add-module-exports": "^1.0.4",
|
||||
"coffeescript": "^1.9.2",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-plugin-no-jquery": "^2.7.0",
|
||||
"mocha": "^9.2.2",
|
||||
"sinon": "^10.0.0"
|
||||
},
|
||||
|
|
|
@ -15,13 +15,24 @@ window.CustomEmbedPlayer = class CustomEmbedPlayer extends EmbedPlayer
|
|||
return
|
||||
|
||||
embedSrc = data.meta.embed.src
|
||||
link = "<a href=\"#{embedSrc}\" target=\"_blank\"><strong>#{embedSrc}</strong></a>"
|
||||
alert = makeAlert('Untrusted Content', CUSTOM_EMBED_WARNING.replace('%link%', link),
|
||||
|
||||
link = document.createElement('a')
|
||||
link.href = embedSrc
|
||||
link.target = '_blank'
|
||||
link.rel = 'noopener noreferer'
|
||||
|
||||
strong = document.createElement('strong')
|
||||
strong.textContent = embedSrc
|
||||
link.appendChild(strong)
|
||||
|
||||
# TODO: Ideally makeAlert() would allow optionally providing a DOM
|
||||
# element instead of requiring HTML text
|
||||
alert = makeAlert('Untrusted Content', CUSTOM_EMBED_WARNING.replace('%link%', link.outerHTML),
|
||||
'alert-warning')
|
||||
.removeClass('col-md-12')
|
||||
$('<button/>').addClass('btn btn-default')
|
||||
.text('Embed')
|
||||
.click(=>
|
||||
.on('click', =>
|
||||
super(data)
|
||||
)
|
||||
.appendTo(alert.find('.alert'))
|
||||
|
|
|
@ -12,7 +12,6 @@ window.DailymotionPlayer = class DailymotionPlayer extends Player
|
|||
|
||||
params =
|
||||
autoplay: 1
|
||||
wmode: if USEROPTS.wmode_transparent then 'transparent' else 'opaque'
|
||||
logo: 0
|
||||
|
||||
quality = @mapQuality(USEROPTS.default_quality)
|
||||
|
|
33
player/iframechild.coffee
Normal file
33
player/iframechild.coffee
Normal file
|
@ -0,0 +1,33 @@
|
|||
window.IframeChild = class IframeChild extends PlayerJSPlayer
|
||||
constructor: (data) ->
|
||||
if not (this instanceof IframeChild)
|
||||
return new IframeChild(data)
|
||||
|
||||
super(data)
|
||||
|
||||
load: (data) ->
|
||||
@setMediaProperties(data)
|
||||
@ready = false
|
||||
|
||||
waitUntilDefined(window, 'playerjs', =>
|
||||
iframe = $('<iframe/>')
|
||||
.attr(
|
||||
src: '/iframe'
|
||||
allow: 'autoplay; fullscreen'
|
||||
)
|
||||
|
||||
removeOld(iframe)
|
||||
@setupFrame(iframe[0], data)
|
||||
@setupPlayer(iframe[0])
|
||||
)
|
||||
|
||||
setupFrame: (iframe, data) ->
|
||||
iframe.addEventListener('load', =>
|
||||
# TODO: ideally, communication with the child frame should use postMessage()
|
||||
iframe.contentWindow.VOLUME = VOLUME
|
||||
iframe.contentWindow.loadMediaPlayer(Object.assign({}, data, { type: 'cm' } ))
|
||||
iframe.contentWindow.document.querySelector('#ytapiplayer').classList.add('vjs-16-9')
|
||||
adapter = iframe.contentWindow.playerjs.VideoJSAdapter(iframe.contentWindow.PLAYER.player)
|
||||
adapter.ready()
|
||||
typeof data?.meta?.thumbnail == 'string' and iframe.contentWindow.PLAYER.player.poster(data.meta.thumbnail)
|
||||
)
|
|
@ -1,12 +0,0 @@
|
|||
window.ImgurPlayer = class ImgurPlayer extends EmbedPlayer
|
||||
constructor: (data) ->
|
||||
if not (this instanceof ImgurPlayer)
|
||||
return new ImgurPlayer(data)
|
||||
|
||||
@load(data)
|
||||
|
||||
load: (data) ->
|
||||
data.meta.embed =
|
||||
tag: 'iframe'
|
||||
src: "https://imgur.com/a/#{data.id}/embed"
|
||||
super(data)
|
|
@ -6,11 +6,12 @@ window.LivestreamPlayer = class LivestreamPlayer extends EmbedPlayer
|
|||
@load(data)
|
||||
|
||||
load: (data) ->
|
||||
[ account, event ] = data.id.split(';')
|
||||
data.meta.embed =
|
||||
src: "https://cdn.livestream.com/embed/#{data.id}?\
|
||||
layout=4&\
|
||||
color=0x000000&\
|
||||
iconColorOver=0xe7e7e7&\
|
||||
iconColor=0xcccccc"
|
||||
src: "https://livestream.com/accounts/#{account}/events/#{event}/player?\
|
||||
enableInfoAndActivity=false&\
|
||||
defaultDrawer=&\
|
||||
autoPlay=true&\
|
||||
mute=false"
|
||||
tag: 'iframe'
|
||||
super(data)
|
||||
|
|
66
player/niconico.coffee
Normal file
66
player/niconico.coffee
Normal 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)
|
21
player/odysee.coffee
Normal file
21
player/odysee.coffee
Normal file
|
@ -0,0 +1,21 @@
|
|||
window.OdyseePlayer = class OdyseePlayer extends PlayerJSPlayer
|
||||
constructor: (data) ->
|
||||
if not (this instanceof OdyseePlayer)
|
||||
return new OdyseePlayer(data)
|
||||
|
||||
super(data)
|
||||
|
||||
load: (data) ->
|
||||
@ready = false
|
||||
@setMediaProperties(data)
|
||||
|
||||
waitUntilDefined(window, 'playerjs', =>
|
||||
iframe = $('<iframe/>')
|
||||
.attr(
|
||||
src: data.meta.embed.src
|
||||
allow: 'autoplay; fullscreen'
|
||||
)
|
||||
|
||||
removeOld(iframe)
|
||||
@setupPlayer(iframe[0], data)
|
||||
)
|
122
player/peertube.coffee
Normal file
122
player/peertube.coffee
Normal file
|
@ -0,0 +1,122 @@
|
|||
PEERTUBE_EMBED_WARNING = 'This channel is embedding PeerTube content from %link%.
|
||||
PeerTube instances may use P2P technology that will expose your IP address to third parties, including but not
|
||||
limited to other users in this channel. It is also conceivable that if the content in question is in violation of
|
||||
copyright laws your IP address could be potentially be observed by legal authorities monitoring the tracker of
|
||||
this PeerTube instance. The operators of %site% are not responsible for the data sent by the embedded player to
|
||||
third parties on your behalf.<br><br> If you understand the risks, wish to assume all liability, and continue to
|
||||
the content, click "Embed" below to allow the content to be embedded.<hr>'
|
||||
|
||||
PEERTUBE_RISK = false
|
||||
|
||||
window.PeerPlayer = class PeerPlayer extends Player
|
||||
constructor: (data) ->
|
||||
if not (this instanceof PeerPlayer)
|
||||
return new PeerPlayer(data)
|
||||
|
||||
@warn(data)
|
||||
|
||||
warn: (data) ->
|
||||
if USEROPTS.peertube_risk or PEERTUBE_RISK
|
||||
return @load(data)
|
||||
|
||||
site = new URL(document.URL).hostname
|
||||
embedSrc = data.meta.embed.domain
|
||||
link = "<a href=\"http://#{embedSrc}\" target=\"_blank\" rel=\"noopener noreferer\"><strong>#{embedSrc}</strong></a>"
|
||||
alert = makeAlert('Privacy Advisory', PEERTUBE_EMBED_WARNING.replace('%link%', link).replace('%site%', site),
|
||||
'alert-warning')
|
||||
.removeClass('col-md-12')
|
||||
$('<button/>').addClass('btn btn-default')
|
||||
.text('Embed')
|
||||
.on('click', =>
|
||||
@load(data)
|
||||
)
|
||||
.appendTo(alert.find('.alert'))
|
||||
$('<button/>').addClass('btn btn-default pull-right')
|
||||
.text('Embed and dont ask again for this session')
|
||||
.on('click', =>
|
||||
PEERTUBE_RISK = true
|
||||
@load(data)
|
||||
)
|
||||
.appendTo(alert.find('.alert'))
|
||||
removeOld(alert)
|
||||
|
||||
load: (data) ->
|
||||
@setMediaProperties(data)
|
||||
|
||||
waitUntilDefined(window, 'PeerTubePlayer', =>
|
||||
video = $('<iframe/>')
|
||||
removeOld(video)
|
||||
video.attr(
|
||||
src: "https://#{data.meta.embed.domain}/videos/embed/#{data.meta.embed.uuid}?api=1"
|
||||
allow: 'autoplay; fullscreen'
|
||||
)
|
||||
|
||||
@peertube = new PeerTubePlayer(video[0])
|
||||
|
||||
@peertube.addEventListener('playbackStatusChange', (status) =>
|
||||
@paused = status == 'paused'
|
||||
if CLIENT.leader
|
||||
sendVideoUpdate()
|
||||
)
|
||||
|
||||
@peertube.addEventListener('playbackStatusUpdate', (status) =>
|
||||
@peertube.currentTime = status.position
|
||||
|
||||
if status.playbackState == "ended" and CLIENT.leader
|
||||
socket.emit('playNext')
|
||||
)
|
||||
|
||||
@peertube.addEventListener('volumeChange', (volume) =>
|
||||
VOLUME = volume
|
||||
setOpt("volume", VOLUME)
|
||||
)
|
||||
|
||||
@play()
|
||||
@setVolume(VOLUME)
|
||||
)
|
||||
|
||||
play: ->
|
||||
@paused = false
|
||||
if @peertube and @peertube.ready
|
||||
@peertube.play().catch((error) ->
|
||||
console.error('PeerTube::play():', error)
|
||||
)
|
||||
|
||||
pause: ->
|
||||
@paused = true
|
||||
if @peertube and @peertube.ready
|
||||
@peertube.pause().catch((error) ->
|
||||
console.error('PeerTube::pause():', error)
|
||||
)
|
||||
|
||||
seekTo: (time) ->
|
||||
if @peertube and @peertube.ready
|
||||
@peertube.seek(time)
|
||||
|
||||
getVolume: (cb) ->
|
||||
if @peertube and @peertube.ready
|
||||
@peertube.getVolume().then((volume) ->
|
||||
cb(parseFloat(volume))
|
||||
).catch((error) ->
|
||||
console.error('PeerTube::getVolume():', error)
|
||||
)
|
||||
else
|
||||
cb(VOLUME)
|
||||
|
||||
setVolume: (volume) ->
|
||||
if @peertube and @peertube.ready
|
||||
@peertube.setVolume(volume).catch((error) ->
|
||||
console.error('PeerTube::setVolume():', error)
|
||||
)
|
||||
|
||||
getTime: (cb) ->
|
||||
if @peertube and @peertube.ready
|
||||
cb(@peertube.currentTime)
|
||||
else
|
||||
cb(0)
|
||||
|
||||
setQuality: (quality) ->
|
||||
# USEROPTS.default_quality
|
||||
# @peertube.getResolutions()
|
||||
# @peertube.setResolution(resolutionId : number)
|
||||
|
|
@ -8,37 +8,31 @@ window.PlayerJSPlayer = class PlayerJSPlayer extends Player
|
|||
load: (data) ->
|
||||
@setMediaProperties(data)
|
||||
@ready = false
|
||||
@finishing = false
|
||||
|
||||
if not data.meta.playerjs
|
||||
throw new Error('Invalid input: missing meta.playerjs')
|
||||
|
||||
waitUntilDefined(window, 'playerjs', =>
|
||||
iframe = $('<iframe/>')
|
||||
.attr(src: data.meta.playerjs.src)
|
||||
.attr(
|
||||
src: data.meta.playerjs.src
|
||||
allow: 'autoplay; fullscreen'
|
||||
)
|
||||
|
||||
removeOld(iframe)
|
||||
@setupPlayer(iframe[0])
|
||||
)
|
||||
|
||||
@player = new playerjs.Player(iframe[0])
|
||||
setupPlayer: (iframe) ->
|
||||
@player = new playerjs.Player(iframe)
|
||||
@player.on('ready', =>
|
||||
@player.on('error', (error) =>
|
||||
console.error('PlayerJS error', error.stack)
|
||||
)
|
||||
@player.on('ended', ->
|
||||
# Streamable seems to not implement this since it loops
|
||||
# gotta use the timeupdate hack below
|
||||
if CLIENT.leader
|
||||
socket.emit('playNext')
|
||||
)
|
||||
@player.on('timeupdate', (time) =>
|
||||
if time.duration - time.seconds < 1 and not @finishing
|
||||
setTimeout(=>
|
||||
if CLIENT.leader
|
||||
socket.emit('playNext')
|
||||
@pause()
|
||||
, (time.duration - time.seconds) * 1000)
|
||||
@finishing = true
|
||||
)
|
||||
@player.on('play', ->
|
||||
@paused = false
|
||||
if CLIENT.leader
|
||||
|
@ -57,7 +51,6 @@ window.PlayerJSPlayer = class PlayerJSPlayer extends Player
|
|||
|
||||
@ready = true
|
||||
)
|
||||
)
|
||||
|
||||
play: ->
|
||||
@paused = false
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
window.SmashcastPlayer = class SmashcastPlayer extends EmbedPlayer
|
||||
constructor: (data) ->
|
||||
if not (this instanceof SmashcastPlayer)
|
||||
return new SmashcastPlayer(data)
|
||||
|
||||
@load(data)
|
||||
|
||||
load: (data) ->
|
||||
data.meta.embed =
|
||||
src: "https://www.smashcast.tv/embed/#{data.id}"
|
||||
tag: 'iframe'
|
||||
super(data)
|
|
@ -6,7 +6,30 @@ window.StreamablePlayer = class StreamablePlayer extends PlayerJSPlayer
|
|||
super(data)
|
||||
|
||||
load: (data) ->
|
||||
data.meta.playerjs =
|
||||
src: "https://streamable.com/e/#{data.id}"
|
||||
@ready = false
|
||||
@finishing = false
|
||||
@setMediaProperties(data)
|
||||
|
||||
super(data)
|
||||
waitUntilDefined(window, 'playerjs', =>
|
||||
iframe = $('<iframe/>')
|
||||
.attr(
|
||||
src: "https://streamable.com/e/#{data.id}"
|
||||
allow: 'autoplay; fullscreen'
|
||||
)
|
||||
|
||||
removeOld(iframe)
|
||||
@setupPlayer(iframe[0])
|
||||
@player.on('ready', =>
|
||||
# Streamable does not implement ended event since it loops
|
||||
# gotta use a timeupdate hack
|
||||
@player.on('timeupdate', (time) =>
|
||||
if time.duration - time.seconds < 1 and not @finishing
|
||||
setTimeout(=>
|
||||
if CLIENT.leader
|
||||
socket.emit('playNext')
|
||||
@pause()
|
||||
, (time.duration - time.seconds) * 1000)
|
||||
@finishing = true
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
|
@ -3,7 +3,6 @@ TYPE_MAP =
|
|||
vi: VimeoPlayer
|
||||
dm: DailymotionPlayer
|
||||
gd: GoogleDrivePlayer
|
||||
gp: VideoJSPlayer
|
||||
fi: FilePlayer
|
||||
sc: SoundCloudPlayer
|
||||
li: LivestreamPlayer
|
||||
|
@ -11,13 +10,15 @@ TYPE_MAP =
|
|||
tv: TwitchPlayer
|
||||
cu: CustomEmbedPlayer
|
||||
rt: RTMPPlayer
|
||||
hb: SmashcastPlayer
|
||||
us: UstreamPlayer
|
||||
im: ImgurPlayer
|
||||
hl: HLSPlayer
|
||||
sb: StreamablePlayer
|
||||
tc: TwitchClipPlayer
|
||||
cm: VideoJSPlayer
|
||||
pt: PeerPlayer
|
||||
bc: IframeChild
|
||||
bn: IframeChild
|
||||
od: OdyseePlayer
|
||||
nv: NicoPlayer
|
||||
|
||||
window.loadMediaPlayer = (data) ->
|
||||
try
|
||||
|
@ -109,7 +110,8 @@ window.removeOld = (replace) ->
|
|||
$('#soundcloud-volume-holder').remove()
|
||||
replace ?= $('<div/>').addClass('embed-responsive-item')
|
||||
old = $('#ytapiplayer')
|
||||
old.attr('id', 'ytapiplayer-old')
|
||||
replace.attr('id', 'ytapiplayer')
|
||||
replace.insertBefore(old)
|
||||
old.remove()
|
||||
replace.attr('id', 'ytapiplayer')
|
||||
return replace
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
window.UstreamPlayer = class UstreamPlayer extends EmbedPlayer
|
||||
constructor: (data) ->
|
||||
if not (this instanceof UstreamPlayer)
|
||||
return new UstreamPlayer(data)
|
||||
|
||||
@load(data)
|
||||
|
||||
load: (data) ->
|
||||
data.meta.embed =
|
||||
tag: 'iframe'
|
||||
src: "https://www.ustream.tv/embed/#{data.id}?html5ui"
|
||||
super(data)
|
|
@ -42,14 +42,14 @@ getSourceLabel = (source) ->
|
|||
else
|
||||
return "#{source.quality}p #{source.contentType.split('/')[1]}"
|
||||
|
||||
waitUntilDefined(window, 'videojs', =>
|
||||
videojs.options.flash.swf = '/video-js.swf'
|
||||
)
|
||||
|
||||
hasAnyTextTracks = (data) ->
|
||||
ntracks = data?.meta?.textTracks?.length ? 0
|
||||
return ntracks > 0
|
||||
|
||||
hasAnyAudioTracks = (data) ->
|
||||
ntracks = data?.meta?.audioTracks?.length ? 0
|
||||
return ntracks > 0
|
||||
|
||||
window.VideoJSPlayer = class VideoJSPlayer extends Player
|
||||
constructor: (data) ->
|
||||
if not (this instanceof VideoJSPlayer)
|
||||
|
@ -112,17 +112,26 @@ window.VideoJSPlayer = class VideoJSPlayer extends Player
|
|||
$('<track/>').attr(attrs).appendTo(video)
|
||||
)
|
||||
|
||||
pluginData =
|
||||
videoJsResolutionSwitcher:
|
||||
default: @sources[0].res
|
||||
|
||||
if hasAnyAudioTracks(data)
|
||||
pluginData.audioSwitch =
|
||||
audioTracks: data.meta.audioTracks,
|
||||
volume: VOLUME
|
||||
|
||||
@player = videojs(video[0],
|
||||
# https://github.com/Dash-Industry-Forum/dash.js/issues/2184
|
||||
autoplay: @sources[0].type != 'application/dash+xml',
|
||||
controls: true,
|
||||
plugins:
|
||||
videoJsResolutionSwitcher:
|
||||
default: @sources[0].res
|
||||
plugins: pluginData
|
||||
|
||||
)
|
||||
@player.ready(=>
|
||||
# Have to use updateSrc instead of <source> tags
|
||||
# see: https://github.com/videojs/video.js/issues/3428
|
||||
@player.poster(data.meta.thumbnail)
|
||||
@player.updateSrc(@sources)
|
||||
@player.on('error', =>
|
||||
err = @player.error()
|
||||
|
|
|
@ -13,14 +13,9 @@ window.VimeoPlayer = class VimeoPlayer extends Player
|
|||
removeOld(video)
|
||||
video.attr(
|
||||
src: "https://player.vimeo.com/video/#{data.id}"
|
||||
webkitallowfullscreen: true
|
||||
mozallowfullscreen: true
|
||||
allowfullscreen: true
|
||||
allow: 'autoplay; fullscreen'
|
||||
)
|
||||
|
||||
if USEROPTS.wmode_transparent
|
||||
video.attr('wmode', 'transparent')
|
||||
|
||||
@vimeo = new Vimeo.Player(video[0])
|
||||
|
||||
@vimeo.on('ended', =>
|
||||
|
|
|
@ -12,7 +12,6 @@ window.YouTubePlayer = class YouTubePlayer extends Player
|
|||
waitUntilDefined(YT, 'Player', =>
|
||||
removeOld()
|
||||
|
||||
wmode = if USEROPTS.wmode_transparent then 'transparent' else 'opaque'
|
||||
@yt = new YT.Player('ytapiplayer',
|
||||
videoId: data.id
|
||||
playerVars:
|
||||
|
@ -21,7 +20,6 @@ window.YouTubePlayer = class YouTubePlayer extends Player
|
|||
controls: 1
|
||||
iv_load_policy: 3 # iv_load_policy 3 indicates no annotations
|
||||
rel: 0
|
||||
wmode: wmode
|
||||
events:
|
||||
onReady: @onReady.bind(this)
|
||||
onStateChange: @onStateChange.bind(this)
|
||||
|
|
1
src/.eslintrc.json
Normal file
1
src/.eslintrc.json
Normal file
|
@ -0,0 +1 @@
|
|||
{ "env": { "node": true } }
|
|
@ -94,7 +94,10 @@ function Channel(name) {
|
|||
}, USERCOUNT_THROTTLE);
|
||||
const self = this;
|
||||
db.channels.load(this, function (err) {
|
||||
if (err && err !== "Channel is not registered") {
|
||||
if (err && err.code === 'EBANNED') {
|
||||
self.emit("loadFail", err.message);
|
||||
self.setFlag(Flags.C_ERROR);
|
||||
} else if (err && err !== "Channel is not registered") {
|
||||
self.emit("loadFail", "Failed to load channel data from the database. Please try again later.");
|
||||
self.setFlag(Flags.C_ERROR);
|
||||
} else {
|
||||
|
|
|
@ -162,7 +162,7 @@ ChatModule.prototype.handleChatMsg = function (user, data) {
|
|||
return;
|
||||
}
|
||||
|
||||
data.msg = data.msg.substring(0, 320);
|
||||
data.msg = data.msg.substring(0, Config.get("max-chat-message-length"));
|
||||
|
||||
// Restrict new accounts/IPs from chatting and posting links
|
||||
if (this.restrictNewAccount(user, data)) {
|
||||
|
@ -248,7 +248,7 @@ ChatModule.prototype.handlePm = function (user, data) {
|
|||
}
|
||||
|
||||
|
||||
data.msg = data.msg.substring(0, 320);
|
||||
data.msg = data.msg.substring(0, Config.get("max-chat-message-length"));
|
||||
var to = null;
|
||||
for (var i = 0; i < this.channel.users.length; i++) {
|
||||
if (this.channel.users[i].getLowerName() === data.to) {
|
||||
|
|
|
@ -4,6 +4,7 @@ var Flags = require("../flags");
|
|||
var util = require("../utilities");
|
||||
var Account = require("../account");
|
||||
import Promise from 'bluebird';
|
||||
const XSS = require("../xss");
|
||||
|
||||
const dbIsNameBanned = Promise.promisify(db.channels.isNameBanned);
|
||||
const dbIsIPBanned = Promise.promisify(db.channels.isIPBanned);
|
||||
|
@ -261,7 +262,6 @@ KickBanModule.prototype.handleCmdIPBan = function (user, msg, _meta) {
|
|||
chan.refCounter.ref("KickBanModule::handleCmdIPBan");
|
||||
|
||||
this.banAll(user, name, range, reason).catch(error => {
|
||||
//console.log('!!!', error.stack);
|
||||
const message = error.message || error;
|
||||
user.socket.emit("errorMsg", { msg: message });
|
||||
}).then(() => {
|
||||
|
@ -276,6 +276,10 @@ KickBanModule.prototype.checkChannelAlive = function checkChannelAlive() {
|
|||
};
|
||||
|
||||
KickBanModule.prototype.banName = async function banName(actor, name, reason) {
|
||||
if (!util.isValidUserName(name)) {
|
||||
throw new Error("Invalid username");
|
||||
}
|
||||
|
||||
reason = reason.substring(0, 255);
|
||||
|
||||
var chan = this.channel;
|
||||
|
@ -323,6 +327,9 @@ KickBanModule.prototype.banName = async function banName(actor, name, reason) {
|
|||
};
|
||||
|
||||
KickBanModule.prototype.banIP = async function banIP(actor, ip, name, reason) {
|
||||
if (!util.isValidUserName(name)) {
|
||||
throw new Error("Invalid username");
|
||||
}
|
||||
reason = reason.substring(0, 255);
|
||||
var masked = util.cloakIP(ip);
|
||||
|
||||
|
@ -404,7 +411,12 @@ KickBanModule.prototype.banAll = async function banAll(
|
|||
);
|
||||
|
||||
if (!await dbIsNameBanned(chan.name, name)) {
|
||||
promises.push(this.banName(actor, name, reason));
|
||||
promises.push(this.banName(actor, name, reason).catch(error => {
|
||||
// TODO: banning should be made idempotent, not throw an error
|
||||
if (!/already banned/.test(error.message)) {
|
||||
throw error;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
@ -440,8 +452,9 @@ KickBanModule.prototype.handleUnban = function (user, data) {
|
|||
self.channel.logger.log("[mod] " + user.getName() + " unbanned " + data.name);
|
||||
if (self.channel.modules.chat) {
|
||||
var banperm = self.channel.modules.permissions.permissions.ban;
|
||||
// TODO: quick fix, shouldn't trust name from unban frame.
|
||||
self.channel.modules.chat.sendModMessage(
|
||||
user.getName() + " unbanned " + data.name,
|
||||
user.getName() + " unbanned " + XSS.sanitizeText(data.name),
|
||||
banperm
|
||||
);
|
||||
}
|
||||
|
|
|
@ -158,11 +158,14 @@ PlaylistModule.prototype.load = function (data) {
|
|||
}
|
||||
} else if (item.media.type === "gd") {
|
||||
delete item.media.meta.gpdirect;
|
||||
} else if (["vm", "jw", "mx", "im"].includes(item.media.type)) {
|
||||
} else if (["vm", "jw", "mx", "im", "gp", "us", "hb"].includes(item.media.type)) {
|
||||
// JW has been deprecated for a long time
|
||||
// VM shut down in December 2017
|
||||
// Mixer shut down in July 2020
|
||||
// Dunno when imgur album embeds stopped working but they don't work either
|
||||
// Imgur replaced albums with a feature called galleries in 2019
|
||||
// Picasa shut down in 2016
|
||||
// Ustream was sunset by IBM in September 2018
|
||||
// SmashCast (Hitbox) seemed to just vanish November 2020
|
||||
LOGGER.warn(
|
||||
"Dropping playlist item with deprecated type %s",
|
||||
item.media.type
|
||||
|
|
24
src/cli/banned-channels.js
Normal file
24
src/cli/banned-channels.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
import Server from '../server';
|
||||
|
||||
export async function handleBanChannel({ name, externalReason, internalReason }) {
|
||||
await Server.getServer().bannedChannelsController.banChannel({
|
||||
name,
|
||||
externalReason,
|
||||
internalReason,
|
||||
bannedBy: '[console]'
|
||||
});
|
||||
|
||||
return { status: 'success' };
|
||||
}
|
||||
|
||||
export async function handleUnbanChannel({ name }) {
|
||||
await Server.getServer().bannedChannelsController.unbanChannel(name, '[console]');
|
||||
|
||||
return { status: 'success' };
|
||||
}
|
||||
|
||||
export async function handleShowBannedChannel({ name }) {
|
||||
let banInfo = await Server.getServer().bannedChannelsController.getBannedChannel(name);
|
||||
|
||||
return { status: 'success', ban: banInfo };
|
||||
}
|
|
@ -11,7 +11,8 @@ import { CaptchaConfig } from './configuration/captchaconfig';
|
|||
const LOGGER = require('@calzoneman/jsli')('config');
|
||||
|
||||
var defaults = {
|
||||
mysql: {
|
||||
database: {
|
||||
client: "mysql",
|
||||
server: "localhost",
|
||||
port: 3306,
|
||||
database: "cytube3",
|
||||
|
@ -66,12 +67,12 @@ var defaults = {
|
|||
}
|
||||
},
|
||||
"youtube-v3-key": "",
|
||||
"channel-blacklist": [],
|
||||
"channel-path": "r",
|
||||
"channel-save-interval": 5,
|
||||
"max-channels-per-user": 5,
|
||||
"max-accounts-per-ip": 5,
|
||||
"guest-login-delay": 60,
|
||||
"max-chat-message-length": 320,
|
||||
aliases: {
|
||||
"purge-interval": 3600000,
|
||||
"max-age": 2592000000
|
||||
|
@ -372,13 +373,6 @@ function preprocessConfig(cfg) {
|
|||
}
|
||||
}
|
||||
|
||||
/* Convert channel blacklist to a hashtable */
|
||||
var tbl = {};
|
||||
cfg["channel-blacklist"].forEach(function (c) {
|
||||
tbl[c.toLowerCase()] = true;
|
||||
});
|
||||
cfg["channel-blacklist"] = tbl;
|
||||
|
||||
/* Check channel path */
|
||||
if(!/^[-\w]+$/.test(cfg["channel-path"])){
|
||||
LOGGER.error("Channel paths may only use the same characters as usernames and channel names.");
|
||||
|
@ -435,6 +429,11 @@ function preprocessConfig(cfg) {
|
|||
cfg['channel-storage'] = { type: undefined };
|
||||
}
|
||||
|
||||
if (cfg["max-chat-message-length"] > 1000) {
|
||||
LOGGER.warn("Max chat message length was greater than 1000. Setting to 1000.");
|
||||
cfg["max-chat-message-length"] = 1000;
|
||||
}
|
||||
|
||||
return cfg;
|
||||
}
|
||||
|
||||
|
|
67
src/controller/banned-channels.js
Normal file
67
src/controller/banned-channels.js
Normal file
|
@ -0,0 +1,67 @@
|
|||
import { eventlog } from '../logger';
|
||||
import { SimpleCache } from '../util/simple-cache';
|
||||
const LOGGER = require('@calzoneman/jsli')('BannedChannelsController');
|
||||
|
||||
export class BannedChannelsController {
|
||||
constructor(dbChannels, globalMessageBus) {
|
||||
this.dbChannels = dbChannels;
|
||||
this.globalMessageBus = globalMessageBus;
|
||||
this.cache = new SimpleCache({
|
||||
maxElem: 1000,
|
||||
maxAge: 5 * 60_000
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* TODO: add an audit log to the database
|
||||
*/
|
||||
|
||||
async banChannel({ name, externalReason, internalReason, bannedBy }) {
|
||||
LOGGER.info(`Banning channel ${name} (banned by ${bannedBy})`);
|
||||
eventlog.log(`[acp] ${bannedBy} banned channel ${name}`);
|
||||
|
||||
let banInfo = await this.dbChannels.getBannedChannel(name);
|
||||
if (banInfo !== null) {
|
||||
LOGGER.warn(`Channel ${name} is already banned, updating ban reason`);
|
||||
}
|
||||
|
||||
this.cache.delete(name);
|
||||
|
||||
await this.dbChannels.putBannedChannel({
|
||||
name,
|
||||
externalReason,
|
||||
internalReason,
|
||||
bannedBy
|
||||
});
|
||||
|
||||
this.globalMessageBus.emit(
|
||||
'ChannelBanned',
|
||||
{ channel: name, externalReason }
|
||||
);
|
||||
}
|
||||
|
||||
async unbanChannel(name, unbannedBy) {
|
||||
LOGGER.info(`Unbanning channel ${name}`);
|
||||
eventlog.log(`[acp] ${unbannedBy} unbanned channel ${name}`);
|
||||
this.cache.delete(name);
|
||||
|
||||
this.globalMessageBus.emit(
|
||||
'ChannelUnbanned',
|
||||
{ channel: name }
|
||||
);
|
||||
|
||||
await this.dbChannels.removeBannedChannel(name);
|
||||
}
|
||||
|
||||
async getBannedChannel(name) {
|
||||
name = name.toLowerCase();
|
||||
|
||||
let info = this.cache.get(name);
|
||||
if (info === null) {
|
||||
info = await this.dbChannels.getBannedChannel(name);
|
||||
this.cache.put(name, info);
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
}
|
|
@ -22,14 +22,23 @@ const SOURCE_CONTENT_TYPES = new Set([
|
|||
'application/dash+xml',
|
||||
'application/x-mpegURL',
|
||||
'audio/aac',
|
||||
'audio/ogg',
|
||||
'audio/mp4',
|
||||
'audio/mpeg',
|
||||
'audio/ogg',
|
||||
'audio/opus',
|
||||
'video/mp4',
|
||||
'video/ogg',
|
||||
'video/webm'
|
||||
]);
|
||||
|
||||
const AUDIO_ONLY_CONTENT_TYPES = new Set([
|
||||
'audio/aac',
|
||||
'audio/mp4',
|
||||
'audio/mpeg',
|
||||
'audio/ogg',
|
||||
'audio/opus'
|
||||
]);
|
||||
|
||||
export function lookup(url, opts) {
|
||||
if (!opts) opts = {};
|
||||
if (!opts.hasOwnProperty('timeout')) opts.timeout = 10000;
|
||||
|
@ -131,6 +140,7 @@ export function convert(id, data) {
|
|||
|
||||
const meta = {
|
||||
direct: sources,
|
||||
audioTracks: data.audioTracks,
|
||||
textTracks: data.textTracks,
|
||||
thumbnail: data.thumbnail, // Currently ignored by Media
|
||||
live: !!data.live // Currently ignored by Media
|
||||
|
@ -160,7 +170,16 @@ export function validate(data) {
|
|||
}
|
||||
|
||||
validateSources(data.sources);
|
||||
validateAudioTracks(data.audioTracks);
|
||||
validateTextTracks(data.textTracks);
|
||||
/*
|
||||
* TODO: Xaekai's Octopus subtitle support uses a separate subTracks array
|
||||
* in a slightly different format than textTracks. That currently requires
|
||||
* a channel script to use, but if that is integrated in core then it needs
|
||||
* to be validated here (and ideally merged with textTracks so there is only
|
||||
* one array).
|
||||
*/
|
||||
validateFonts(data.fonts);
|
||||
}
|
||||
|
||||
function validateSources(sources) {
|
||||
|
@ -179,7 +198,8 @@ function validateSources(sources) {
|
|||
`unacceptable source contentType "${source.contentType}"`
|
||||
);
|
||||
|
||||
if (!SOURCE_QUALITIES.has(source.quality))
|
||||
// TODO (Xaekai): This should be allowed
|
||||
if (/*!AUDIO_ONLY_CONTENT_TYPES.has(source.contentType) && */!SOURCE_QUALITIES.has(source.quality))
|
||||
throw new ValidationError(`unacceptable source quality "${source.quality}"`);
|
||||
|
||||
if (source.hasOwnProperty('bitrate')) {
|
||||
|
@ -193,6 +213,45 @@ function validateSources(sources) {
|
|||
}
|
||||
}
|
||||
|
||||
function validateAudioTracks(audioTracks) {
|
||||
if (typeof audioTracks === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Array.isArray(audioTracks)){
|
||||
throw new ValidationError('audioTracks must be a list');
|
||||
}
|
||||
|
||||
for (let track of audioTracks) {
|
||||
if (typeof track.url !== 'string'){
|
||||
throw new ValidationError('audio track URL must be a string');
|
||||
}
|
||||
validateURL(track.url);
|
||||
|
||||
if (!AUDIO_ONLY_CONTENT_TYPES.has(track.contentType)){
|
||||
throw new ValidationError(
|
||||
`unacceptable audio track contentType "${track.contentType}"`
|
||||
);
|
||||
}
|
||||
if (typeof track.label !== 'string'){
|
||||
throw new ValidationError('audio track label must be a string');
|
||||
}
|
||||
if (!track.label){
|
||||
throw new ValidationError('audio track label must be nonempty');
|
||||
}
|
||||
|
||||
if (typeof track.language !== 'string'){
|
||||
throw new ValidationError('audio track language must be a string');
|
||||
}
|
||||
if (!track.language){
|
||||
throw new ValidationError('audio track language must be nonempty');
|
||||
}
|
||||
if (!/^[a-z]{2,3}$/.test(track.language)){
|
||||
throw new ValidationError('audio track language must be a two or three letter IETF BCP 47 subtag');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function validateTextTracks(textTracks) {
|
||||
if (typeof textTracks === 'undefined') {
|
||||
return;
|
||||
|
@ -228,6 +287,20 @@ function validateTextTracks(textTracks) {
|
|||
}
|
||||
}
|
||||
|
||||
function validateFonts(fonts) {
|
||||
if (typeof textTracks === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Array.isArray(fonts))
|
||||
throw new ValidationError('fonts must be a list of URLs');
|
||||
|
||||
for (let f of fonts) {
|
||||
if (typeof f !== 'string')
|
||||
throw new ValidationError('fonts must be a list of URLs');
|
||||
}
|
||||
}
|
||||
|
||||
function parseURL(urlstring) {
|
||||
const url = urlParse(urlstring);
|
||||
|
||||
|
|
|
@ -30,19 +30,19 @@ class Database {
|
|||
constructor(knexConfig = null) {
|
||||
if (knexConfig === null) {
|
||||
knexConfig = {
|
||||
client: 'mysql',
|
||||
client: Config.get('database.client'),
|
||||
connection: {
|
||||
host: Config.get('mysql.server'),
|
||||
port: Config.get('mysql.port'),
|
||||
user: Config.get('mysql.user'),
|
||||
password: Config.get('mysql.password'),
|
||||
database: Config.get('mysql.database'),
|
||||
host: Config.get('database.server'),
|
||||
port: Config.get('database.port'),
|
||||
user: Config.get('database.user'),
|
||||
password: Config.get('database.password'),
|
||||
database: Config.get('database.database'),
|
||||
multipleStatements: true, // Legacy thing
|
||||
charset: 'utf8mb4'
|
||||
},
|
||||
pool: {
|
||||
min: Config.get('mysql.pool-size'),
|
||||
max: Config.get('mysql.pool-size')
|
||||
min: Config.get('database.pool-size'),
|
||||
max: Config.get('database.pool-size')
|
||||
},
|
||||
debug: !!process.env.KNEX_DEBUG
|
||||
};
|
||||
|
@ -73,6 +73,8 @@ module.exports.init = function (newDB) {
|
|||
} else {
|
||||
db = new Database();
|
||||
}
|
||||
// FIXME Initial database connection failed: error: select 1 from dual
|
||||
// relation "dual" does not exist
|
||||
db.knex.raw('select 1 from dual')
|
||||
.catch(error => {
|
||||
LOGGER.error('Initial database connection failed: %s', error.stack);
|
||||
|
@ -85,6 +87,9 @@ module.exports.init = function (newDB) {
|
|||
require('@cytube/mediaquery/lib/provider/youtube').setCache(
|
||||
new MetadataCacheDB(db)
|
||||
);
|
||||
require('@cytube/mediaquery/lib/provider/bitchute').setCache(
|
||||
new MetadataCacheDB(db)
|
||||
);
|
||||
}).catch(error => {
|
||||
LOGGER.error(error.stack);
|
||||
process.exit(1);
|
||||
|
|
|
@ -2,6 +2,7 @@ var db = require("../database");
|
|||
var valid = require("../utilities").isValidChannelName;
|
||||
var Flags = require("../flags");
|
||||
var util = require("../utilities");
|
||||
// TODO: I think newer knex has native support for this
|
||||
import { createMySQLDuplicateKeyUpdate } from '../util/on-duplicate-key-update';
|
||||
import Config from '../config';
|
||||
|
||||
|
@ -209,7 +210,9 @@ module.exports = {
|
|||
return;
|
||||
}
|
||||
|
||||
db.query("SELECT * FROM `channels` WHERE owner=?", [owner],
|
||||
db.query("SELECT c.*, bc.external_reason as banReason " +
|
||||
"FROM channels c LEFT OUTER JOIN banned_channels bc " +
|
||||
"ON bc.channel_name = c.name WHERE c.owner=?", [owner],
|
||||
function (err, res) {
|
||||
if (err) {
|
||||
callback(err, []);
|
||||
|
@ -245,13 +248,28 @@ module.exports = {
|
|||
return;
|
||||
}
|
||||
|
||||
db.query("SELECT * FROM `channels` WHERE name=?", chan.name, function (err, res) {
|
||||
db.query("SELECT c.*, bc.external_reason as banReason " +
|
||||
"FROM channels c LEFT OUTER JOIN banned_channels bc " +
|
||||
"ON bc.channel_name = c.name WHERE c.name=? " +
|
||||
"UNION " +
|
||||
"SELECT c.*, bc.external_reason as banReason " +
|
||||
"FROM channels c RIGHT OUTER JOIN banned_channels bc " +
|
||||
"ON bc.channel_name = c.name WHERE bc.channel_name=? ",
|
||||
[chan.name, chan.name],
|
||||
function (err, res) {
|
||||
if (err) {
|
||||
callback(err, null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (res.length === 0) {
|
||||
if (res.length > 0 && res[0].banReason !== null) {
|
||||
let banError = new Error(`Channel is banned: ${res[0].banReason}`);
|
||||
banError.code = 'EBANNED';
|
||||
callback(banError, null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (res.length === 0 || res[0].id === null) {
|
||||
callback("Channel is not registered", null);
|
||||
return;
|
||||
}
|
||||
|
@ -704,5 +722,63 @@ module.exports = {
|
|||
LOGGER.error(`Failed to update owner_last_seen column for channel ID ${channelId}: ${error}`);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
getBannedChannel: async function getBannedChannel(name) {
|
||||
if (!valid(name)) {
|
||||
throw new Error("Invalid channel name");
|
||||
}
|
||||
|
||||
return await db.getDB().runTransaction(async tx => {
|
||||
let rows = await tx.table('banned_channels')
|
||||
.where({ channel_name: name })
|
||||
.select();
|
||||
|
||||
if (rows.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
channelName: rows[0].channel_name,
|
||||
externalReason: rows[0].external_reason,
|
||||
internalReason: rows[0].internal_reason,
|
||||
bannedBy: rows[0].banned_by,
|
||||
createdAt: rows[0].created_at,
|
||||
updatedAt: rows[0].updated_at
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
putBannedChannel: async function putBannedChannel({ name, externalReason, internalReason, bannedBy }) {
|
||||
if (!valid(name)) {
|
||||
throw new Error("Invalid channel name");
|
||||
}
|
||||
|
||||
return await db.getDB().runTransaction(async tx => {
|
||||
let insert = tx.table('banned_channels')
|
||||
.insert({
|
||||
channel_name: name,
|
||||
external_reason: externalReason,
|
||||
internal_reason: internalReason,
|
||||
banned_by: bannedBy
|
||||
});
|
||||
let update = tx.raw(createMySQLDuplicateKeyUpdate(
|
||||
['external_reason', 'internal_reason', 'banned_by']
|
||||
));
|
||||
|
||||
return tx.raw(insert.toString() + update.toString());
|
||||
});
|
||||
},
|
||||
|
||||
removeBannedChannel: async function removeBannedChannel(name) {
|
||||
if (!valid(name)) {
|
||||
throw new Error("Invalid channel name");
|
||||
}
|
||||
|
||||
return await db.getDB().runTransaction(async tx => {
|
||||
await tx.table('banned_channels')
|
||||
.where({ channel_name: name })
|
||||
.delete();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -9,6 +9,8 @@ function mediaquery2cytube(type) {
|
|||
switch (type) {
|
||||
case 'youtube':
|
||||
return 'yt';
|
||||
case 'bitchute':
|
||||
return 'bc';
|
||||
default:
|
||||
throw new Error(`mediaquery2cytube: no mapping for ${type}`);
|
||||
}
|
||||
|
@ -18,14 +20,17 @@ function cytube2mediaquery(type) {
|
|||
switch (type) {
|
||||
case 'yt':
|
||||
return 'youtube';
|
||||
case 'bc':
|
||||
return 'bitchute';
|
||||
default:
|
||||
throw new Error(`cytube2mediaquery: no mapping for ${type}`);
|
||||
}
|
||||
}
|
||||
|
||||
const cachedResultAge = new Summary({
|
||||
name: 'cytube_yt_cache_result_age_seconds',
|
||||
help: 'Age (in seconds) of cached record'
|
||||
name: 'cytube_media_cache_result_age_seconds',
|
||||
help: 'Age (in seconds) of cached record',
|
||||
labelNames: ['source']
|
||||
});
|
||||
|
||||
class MetadataCacheDB {
|
||||
|
@ -62,10 +67,11 @@ class MetadataCacheDB {
|
|||
return null;
|
||||
}
|
||||
|
||||
let age = 0;
|
||||
try {
|
||||
let age = (Date.now() - row.updated_at.getTime())/1000;
|
||||
age = (Date.now() - row.updated_at.getTime())/1000;
|
||||
if (age > 0) {
|
||||
cachedResultAge.observe(age);
|
||||
cachedResultAge.labels(type).observe(age);
|
||||
}
|
||||
} catch (error) {
|
||||
LOGGER.error('Failed to record cachedResultAge metric: %s', error.stack);
|
||||
|
@ -73,6 +79,7 @@ class MetadataCacheDB {
|
|||
|
||||
let metadata = JSON.parse(row.metadata);
|
||||
metadata.type = cytube2mediaquery(metadata.type);
|
||||
metadata.meta.cacheAge = age;
|
||||
return new Media(metadata);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -156,4 +156,15 @@ export async function initTables() {
|
|||
t.primary(['type', 'id']);
|
||||
t.index('updated_at');
|
||||
});
|
||||
|
||||
await ensureTable('banned_channels', t => {
|
||||
t.charset('utf8mb4');
|
||||
t.string('channel_name', 30)
|
||||
.notNullable()
|
||||
.unique();
|
||||
t.text('external_reason').notNullable();
|
||||
t.text('internal_reason').notNullable();
|
||||
t.string('banned_by', 20).notNullable();
|
||||
t.timestamps(/* useTimestamps */ true, /* defaultToNow */ true);
|
||||
});
|
||||
}
|
||||
|
|
130
src/get-info.js
130
src/get-info.js
|
@ -6,6 +6,11 @@ const ffmpeg = require("./ffmpeg");
|
|||
const mediaquery = require("@cytube/mediaquery");
|
||||
const YouTube = require("@cytube/mediaquery/lib/provider/youtube");
|
||||
const Vimeo = require("@cytube/mediaquery/lib/provider/vimeo");
|
||||
const Odysee = require("@cytube/mediaquery/lib/provider/odysee");
|
||||
const PeerTube = require("@cytube/mediaquery/lib/provider/peertube");
|
||||
const BitChute = require("@cytube/mediaquery/lib/provider/bitchute");
|
||||
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 TwitchVOD = require("@cytube/mediaquery/lib/provider/twitch-vod");
|
||||
const TwitchClip = require("@cytube/mediaquery/lib/provider/twitch-clip");
|
||||
|
@ -214,14 +219,12 @@ var Getters = {
|
|||
|
||||
/* livestream.com */
|
||||
li: function (id, callback) {
|
||||
var m = id.match(/([\w-]+)/);
|
||||
if (m) {
|
||||
id = m[1];
|
||||
} else {
|
||||
if (!id.match(/^\d+;\d+$/)) {
|
||||
callback("Invalid ID", null);
|
||||
return;
|
||||
}
|
||||
var title = "Livestream.com - " + id;
|
||||
|
||||
var title = "Livestream.com";
|
||||
var media = new Media(id, title, "--:--", "li");
|
||||
callback(false, media);
|
||||
},
|
||||
|
@ -278,47 +281,6 @@ var Getters = {
|
|||
});
|
||||
},
|
||||
|
||||
/* ustream.tv */
|
||||
us: function (id, callback) {
|
||||
var m = id.match(/(channel\/[^?&#]+)/);
|
||||
if (m) {
|
||||
id = m[1];
|
||||
} else {
|
||||
callback("Invalid ID", null);
|
||||
return;
|
||||
}
|
||||
|
||||
var options = {
|
||||
host: "www.ustream.tv",
|
||||
port: 443,
|
||||
path: "/" + id,
|
||||
method: "GET",
|
||||
timeout: 1000
|
||||
};
|
||||
|
||||
urlRetrieve(https, options, function (status, data) {
|
||||
if(status !== 200) {
|
||||
callback("Ustream HTTP " + status, null);
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* Yes, regexing this information out of the HTML sucks.
|
||||
* No, there is not a better solution -- it seems IBM
|
||||
* deprecated the old API (or at least replaced with an
|
||||
* enterprise API marked "Contact sales") so fuck it.
|
||||
*/
|
||||
var m = data.match(/https:\/\/www\.ustream\.tv\/embed\/(\d+)/);
|
||||
if (m) {
|
||||
var title = "Ustream.tv - " + id;
|
||||
var media = new Media(m[1], title, "--:--", "us");
|
||||
callback(false, media);
|
||||
} else {
|
||||
callback("Channel ID not found", null);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/* rtmp stream */
|
||||
rt: function (id, callback) {
|
||||
var title = "Livestream";
|
||||
|
@ -391,28 +353,6 @@ var Getters = {
|
|||
});
|
||||
},
|
||||
|
||||
/* hitbox.tv / smashcast.tv */
|
||||
hb: function (id, callback) {
|
||||
var m = id.match(/([\w-]+)/);
|
||||
if (m) {
|
||||
id = m[1];
|
||||
} else {
|
||||
callback("Invalid ID", null);
|
||||
return;
|
||||
}
|
||||
var title = "Smashcast - " + id;
|
||||
var media = new Media(id, title, "--:--", "hb");
|
||||
callback(false, media);
|
||||
},
|
||||
|
||||
/* vid.me */
|
||||
vm: function (id, callback) {
|
||||
process.nextTick(
|
||||
callback,
|
||||
"As of December 2017, vid.me is no longer in service."
|
||||
);
|
||||
},
|
||||
|
||||
/* streamable */
|
||||
sb: function (id, callback) {
|
||||
if (!/^[\w-]+$/.test(id)) {
|
||||
|
@ -429,6 +369,16 @@ var Getters = {
|
|||
});
|
||||
},
|
||||
|
||||
/* PeerTube network */
|
||||
pt: function (id, callback) {
|
||||
PeerTube.lookup(id).then(video => {
|
||||
video = new Media(video.id, video.title, video.duration, "pt", video.meta);
|
||||
callback(null, video);
|
||||
}).catch(error => {
|
||||
callback(error.message || error);
|
||||
});
|
||||
},
|
||||
|
||||
/* custom media - https://github.com/calzoneman/sync/issues/655 */
|
||||
cm: async function (id, callback) {
|
||||
try {
|
||||
|
@ -439,12 +389,44 @@ var Getters = {
|
|||
}
|
||||
},
|
||||
|
||||
/* mixer.com */
|
||||
mx: function (id, callback) {
|
||||
process.nextTick(
|
||||
callback,
|
||||
"As of July 2020, Mixer is no longer in service."
|
||||
);
|
||||
/* BitChute */
|
||||
bc: function (id, callback) {
|
||||
BitChute.lookup(id).then(video => {
|
||||
video = new Media(video.id, video.title, video.duration, "bc", video.meta);
|
||||
callback(null, video);
|
||||
}).catch(error => {
|
||||
callback(error.message || error);
|
||||
});
|
||||
},
|
||||
|
||||
/* Odysee */
|
||||
od: function (id, callback) {
|
||||
Odysee.lookup(id).then(video => {
|
||||
video = new Media(video.id, video.title, video.duration, "od", video.meta);
|
||||
callback(null, video);
|
||||
}).catch(error => {
|
||||
callback(error.message || error);
|
||||
});
|
||||
},
|
||||
|
||||
/* BandCamp */
|
||||
bn: function (id, callback) {
|
||||
BandCamp.lookup(id).then(video => {
|
||||
video = new Media(video.id, video.title, video.duration, "bn", video.meta);
|
||||
callback(null, video);
|
||||
}).catch(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);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -196,7 +196,7 @@ class IOServer {
|
|||
|
||||
handleConnection(socket) {
|
||||
if (!this.checkIPLimit(socket)) {
|
||||
//return;
|
||||
return;
|
||||
}
|
||||
|
||||
patchTypecheckedFunctions(socket);
|
||||
|
|
36
src/main.js
36
src/main.js
|
@ -2,6 +2,7 @@ import Config from './config';
|
|||
import * as Switches from './switches';
|
||||
import { eventlog } from './logger';
|
||||
require('source-map-support').install();
|
||||
import * as bannedChannels from './cli/banned-channels';
|
||||
|
||||
const LOGGER = require('@calzoneman/jsli')('main');
|
||||
|
||||
|
@ -28,10 +29,39 @@ if (!Config.get('debug')) {
|
|||
});
|
||||
}
|
||||
|
||||
async function handleCliCmd(cmd) {
|
||||
try {
|
||||
switch (cmd.command) {
|
||||
case 'ban-channel':
|
||||
return bannedChannels.handleBanChannel(cmd);
|
||||
case 'unban-channel':
|
||||
return bannedChannels.handleUnbanChannel(cmd);
|
||||
case 'show-banned-channel':
|
||||
return bannedChannels.handleShowBannedChannel(cmd);
|
||||
default:
|
||||
throw new Error(`Unrecognized command "${cmd.command}"`);
|
||||
}
|
||||
} catch (error) {
|
||||
return { status: 'error', error: String(error) };
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: this can probably just be part of servsock.js
|
||||
// servsock should also be refactored to send replies instead of
|
||||
// relying solely on tailing logs
|
||||
function handleLine(line) {
|
||||
function handleLine(line, client) {
|
||||
try {
|
||||
let cmd = JSON.parse(line);
|
||||
handleCliCmd(cmd).then(res => {
|
||||
client.write(JSON.stringify(res) + '\n');
|
||||
}).catch(error => {
|
||||
LOGGER.error(`Unexpected error in handleCliCmd: ${error.stack}`);
|
||||
client.write('{"status":"error","error":"internal error"}\n');
|
||||
});
|
||||
} catch (_error) {
|
||||
// eslint no-empty: off
|
||||
}
|
||||
|
||||
if (line === '/reload') {
|
||||
LOGGER.info('Reloading config');
|
||||
try {
|
||||
|
@ -81,9 +111,9 @@ if (Config.get('service-socket.enabled')) {
|
|||
const ServiceSocket = require('./servsock');
|
||||
const sock = new ServiceSocket();
|
||||
sock.init(
|
||||
line => {
|
||||
(line, client) => {
|
||||
try {
|
||||
handleLine(line);
|
||||
handleLine(line, client);
|
||||
} catch (error) {
|
||||
LOGGER.error(
|
||||
'Error in UNIX socket command handler: %s',
|
||||
|
|
|
@ -39,7 +39,7 @@ Media.prototype = {
|
|||
embed: this.meta.embed,
|
||||
gdrive_subtitles: this.meta.gdrive_subtitles,
|
||||
textTracks: this.meta.textTracks,
|
||||
mixer: this.meta.mixer
|
||||
audioTracks: this.meta.audioTracks
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -54,6 +54,11 @@ Media.prototype = {
|
|||
result.meta.direct = this.meta.direct;
|
||||
}
|
||||
|
||||
// Only save thumbnails for items which can be audio track only
|
||||
if (['bn','cm'].includes(this.type)) {
|
||||
result.meta.thumbnail = this.meta.thumbnail;
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
|
|
29
src/peertubelist.js
Normal file
29
src/peertubelist.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
import { fetchPeertubeDomains, setDomains } from '@cytube/mediaquery/lib/provider/peertube';
|
||||
import { stat, readFile, writeFile } from 'node:fs/promises';
|
||||
import path from 'path';
|
||||
|
||||
const LOGGER = require('@calzoneman/jsli')('peertubelist');
|
||||
const ONE_DAY = 24 * 3600 * 1000;
|
||||
const FILENAME = path.join(__dirname, '..', 'peertube-hosts.json');
|
||||
|
||||
export async function setupPeertubeDomains() {
|
||||
try {
|
||||
let mtime;
|
||||
try {
|
||||
mtime = (await stat(FILENAME)).mtime;
|
||||
} catch (_error) {
|
||||
mtime = 0;
|
||||
}
|
||||
|
||||
if (Date.now() - mtime > ONE_DAY) {
|
||||
LOGGER.info('Updating peertube host list');
|
||||
const hosts = await fetchPeertubeDomains();
|
||||
await writeFile(FILENAME, JSON.stringify(hosts));
|
||||
}
|
||||
|
||||
const hosts = JSON.parse(await readFile(FILENAME));
|
||||
setDomains(hosts);
|
||||
} catch (error) {
|
||||
LOGGER.error('Failed to initialize peertube host list: %s', error.stack);
|
||||
}
|
||||
}
|
|
@ -48,6 +48,7 @@ import { PartitionModule } from './partition/partitionmodule';
|
|||
import { Gauge } from 'prom-client';
|
||||
import { EmailController } from './controller/email';
|
||||
import { CaptchaController } from './controller/captcha';
|
||||
import { BannedChannelsController } from './controller/banned-channels';
|
||||
|
||||
var Server = function () {
|
||||
var self = this;
|
||||
|
@ -71,6 +72,7 @@ var Server = function () {
|
|||
const globalMessageBus = this.initModule.getGlobalMessageBus();
|
||||
globalMessageBus.on('UserProfileChanged', this.handleUserProfileChange.bind(this));
|
||||
globalMessageBus.on('ChannelDeleted', this.handleChannelDelete.bind(this));
|
||||
globalMessageBus.on('ChannelBanned', this.handleChannelBanned.bind(this));
|
||||
globalMessageBus.on('ChannelRegistered', this.handleChannelRegister.bind(this));
|
||||
|
||||
// database init ------------------------------------------------------
|
||||
|
@ -108,6 +110,11 @@ var Server = function () {
|
|||
Config.getCaptchaConfig()
|
||||
);
|
||||
|
||||
self.bannedChannelsController = new BannedChannelsController(
|
||||
self.db.channels,
|
||||
globalMessageBus
|
||||
);
|
||||
|
||||
// webserver init -----------------------------------------------------
|
||||
const ioConfig = IOConfiguration.fromOldConfig(Config);
|
||||
const webConfig = WebConfiguration.fromOldConfig(Config);
|
||||
|
@ -134,7 +141,8 @@ var Server = function () {
|
|||
Config.getEmailConfig(),
|
||||
emailController,
|
||||
Config.getCaptchaConfig(),
|
||||
captchaController
|
||||
captchaController,
|
||||
self.bannedChannelsController
|
||||
);
|
||||
|
||||
// http/https/sio server init -----------------------------------------
|
||||
|
@ -204,6 +212,8 @@ var Server = function () {
|
|||
// background tasks init ----------------------------------------------
|
||||
require("./bgtask")(self);
|
||||
|
||||
require("./peertubelist").setupPeertubeDomains().then(() => {});
|
||||
|
||||
// prometheus server
|
||||
const prometheusConfig = Config.getPrometheusConfig();
|
||||
if (prometheusConfig.isEnabled()) {
|
||||
|
@ -547,6 +557,34 @@ Server.prototype.handleChannelDelete = function (event) {
|
|||
}
|
||||
};
|
||||
|
||||
Server.prototype.handleChannelBanned = function (event) {
|
||||
try {
|
||||
const lname = event.channel.toLowerCase();
|
||||
const reason = event.externalReason;
|
||||
|
||||
this.channels.forEach(channel => {
|
||||
if (channel.dead) return;
|
||||
|
||||
if (channel.uniqueName === lname) {
|
||||
channel.clearFlag(Flags.C_REGISTERED);
|
||||
|
||||
const users = Array.prototype.slice.call(channel.users);
|
||||
users.forEach(u => {
|
||||
u.kick(`Channel was banned: ${reason}`);
|
||||
});
|
||||
|
||||
if (!channel.dead && !channel.dying) {
|
||||
channel.emit('empty');
|
||||
}
|
||||
|
||||
LOGGER.info('Processed banned channel %s', lname);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
LOGGER.error('handleChannelBanned failed: %s', error);
|
||||
}
|
||||
};
|
||||
|
||||
Server.prototype.handleChannelRegister = function (event) {
|
||||
try {
|
||||
const lname = event.channel.toLowerCase();
|
||||
|
|
|
@ -34,7 +34,7 @@ export default class ServiceSocket {
|
|||
delete this.connections[id];
|
||||
});
|
||||
stream.on('data', (msg) => {
|
||||
this.handler(msg.toString());
|
||||
this.handler(msg.toString(), stream);
|
||||
});
|
||||
}).listen(this.socket);
|
||||
process.on('exit', this.closeServiceSocket.bind(this));
|
||||
|
|
|
@ -76,10 +76,6 @@ User.prototype.handleJoinChannel = function handleJoinChannel(data) {
|
|||
}
|
||||
|
||||
data.name = data.name.toLowerCase();
|
||||
if (data.name in Config.get("channel-blacklist")) {
|
||||
this.kick("This channel is blacklisted.");
|
||||
return;
|
||||
}
|
||||
|
||||
this.waitFlag(Flags.U_READY, () => {
|
||||
var chan;
|
||||
|
@ -102,10 +98,6 @@ User.prototype.handleJoinChannel = function handleJoinChannel(data) {
|
|||
|
||||
if (!chan.is(Flags.C_READY)) {
|
||||
chan.once("loadFail", reason => {
|
||||
this.socket.emit("errorMsg", {
|
||||
msg: reason,
|
||||
alert: true
|
||||
});
|
||||
this.kick(`Channel could not be loaded: ${reason}`);
|
||||
});
|
||||
}
|
||||
|
|
45
src/util/simple-cache.js
Normal file
45
src/util/simple-cache.js
Normal file
|
@ -0,0 +1,45 @@
|
|||
class SimpleCache {
|
||||
constructor({ maxElem, maxAge }) {
|
||||
this.maxElem = maxElem;
|
||||
this.maxAge = maxAge;
|
||||
this.cache = new Map();
|
||||
|
||||
setInterval(() => {
|
||||
this.cleanup();
|
||||
}, maxAge).unref();
|
||||
}
|
||||
|
||||
put(key, value) {
|
||||
this.cache.set(key, { value: value, at: Date.now() });
|
||||
|
||||
if (this.cache.size > this.maxElem) {
|
||||
this.cache.delete(this.cache.keys().next().value);
|
||||
}
|
||||
}
|
||||
|
||||
get(key) {
|
||||
let val = this.cache.get(key);
|
||||
|
||||
if (val != null && Date.now() < val.at + this.maxAge) {
|
||||
return val.value;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
delete(key) {
|
||||
this.cache.delete(key);
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
let now = Date.now();
|
||||
|
||||
for (let [key, value] of this.cache) {
|
||||
if (value.at < now - this.maxAge) {
|
||||
this.cache.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { SimpleCache };
|
|
@ -193,14 +193,10 @@
|
|||
return "https://twitch.tv/" + id;
|
||||
case "rt":
|
||||
return id;
|
||||
case "us":
|
||||
return "https://ustream.tv/channel/" + id;
|
||||
case "gd":
|
||||
return "https://docs.google.com/file/d/" + id;
|
||||
case "fi":
|
||||
return id;
|
||||
case "hb":
|
||||
return "https://www.smashcast.tv/" + id;
|
||||
case "hl":
|
||||
return id;
|
||||
case "sb":
|
||||
|
@ -209,6 +205,22 @@
|
|||
return "https://clips.twitch.tv/" + id;
|
||||
case "cm":
|
||||
return id;
|
||||
case "pt": {
|
||||
const [domain,uuid] = id.split(';');
|
||||
return `https://${domain}/videos/watch/${uuid}`;
|
||||
}
|
||||
case "bc":
|
||||
return `https://www.bitchute.com/video/${id}/`;
|
||||
case "bn": {
|
||||
const [artist,track] = id.split(';');
|
||||
return `https://${artist}.bandcamp.com/track/${track}`;
|
||||
}
|
||||
case "od": {
|
||||
const [user,video] = id.split(';');
|
||||
return `https://odysee.com/@${user}/${video}`;
|
||||
}
|
||||
case "nv":
|
||||
return `https://www.nicovideo.jp/watch/${id}`;
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
|
@ -218,10 +230,8 @@
|
|||
switch (type) {
|
||||
case "li":
|
||||
case "tw":
|
||||
case "us":
|
||||
case "rt":
|
||||
case "cu":
|
||||
case "hb":
|
||||
case "hl":
|
||||
return true;
|
||||
default:
|
||||
|
|
|
@ -265,6 +265,8 @@ async function handleNewChannel(req, res) {
|
|||
});
|
||||
}
|
||||
|
||||
let banInfo = await db.channels.getBannedChannel(name);
|
||||
|
||||
db.channels.listUserChannels(user.name, function (err, channels) {
|
||||
if (err) {
|
||||
sendPug(res, "account-channels", {
|
||||
|
@ -274,6 +276,14 @@ async function handleNewChannel(req, res) {
|
|||
return;
|
||||
}
|
||||
|
||||
if (banInfo !== null) {
|
||||
sendPug(res, "account-channels", {
|
||||
channels: channels,
|
||||
newChannelError: `Cannot register "${name}": this channel is banned.`
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (name.match(Config.get("reserved-names.channels"))) {
|
||||
sendPug(res, "account-channels", {
|
||||
channels: channels,
|
||||
|
|
|
@ -1,16 +1,25 @@
|
|||
import CyTubeUtil from '../../utilities';
|
||||
import Config from '../../config';
|
||||
import { sanitizeText } from '../../xss';
|
||||
import { sendPug } from '../pug';
|
||||
import * as HTTPStatus from '../httpstatus';
|
||||
import { HTTPError } from '../../errors';
|
||||
|
||||
export default function initialize(app, ioConfig, chanPath) {
|
||||
app.get(`/${chanPath}/:channel`, (req, res) => {
|
||||
export default function initialize(app, ioConfig, chanPath, getBannedChannel) {
|
||||
app.get(`/${chanPath}/:channel`, async (req, res) => {
|
||||
if (!req.params.channel || !CyTubeUtil.isValidChannelName(req.params.channel)) {
|
||||
throw new HTTPError(`"${sanitizeText(req.params.channel)}" is not a valid ` +
|
||||
'channel name.', { status: HTTPStatus.NOT_FOUND });
|
||||
}
|
||||
|
||||
let banInfo = await getBannedChannel(req.params.channel);
|
||||
if (banInfo !== null) {
|
||||
sendPug(res, 'banned_channel', {
|
||||
externalReason: banInfo.externalReason
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const endpoints = ioConfig.getSocketEndpoints();
|
||||
if (endpoints.length === 0) {
|
||||
throw new HTTPError('No socket.io endpoints configured');
|
||||
|
@ -19,7 +28,8 @@ export default function initialize(app, ioConfig, chanPath) {
|
|||
|
||||
sendPug(res, 'channel', {
|
||||
channelName: req.params.channel,
|
||||
sioSource: `${socketBaseURL}/socket.io/socket.io.js`
|
||||
sioSource: `${socketBaseURL}/socket.io/socket.io.js`,
|
||||
maxMsgLen: Config.get("max-chat-message-length")
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
7
src/web/routes/iframe.js
Normal file
7
src/web/routes/iframe.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { sendPug } from '../pug';
|
||||
|
||||
export default function initialize(app) {
|
||||
app.get('/iframe', (req, res) => {
|
||||
return sendPug(res, 'iframe');
|
||||
});
|
||||
}
|
|
@ -143,7 +143,8 @@ module.exports = {
|
|||
emailConfig,
|
||||
emailController,
|
||||
captchaConfig,
|
||||
captchaController
|
||||
captchaController,
|
||||
bannedChannelsController
|
||||
) {
|
||||
patchExpressToHandleAsync();
|
||||
const chanPath = Config.get('channel-path');
|
||||
|
@ -194,7 +195,12 @@ module.exports = {
|
|||
LOGGER.info('Enabled express-minify for CSS and JS');
|
||||
}
|
||||
|
||||
require('./routes/channel')(app, ioConfig, chanPath);
|
||||
require('./routes/channel')(
|
||||
app,
|
||||
ioConfig,
|
||||
chanPath,
|
||||
async name => bannedChannelsController.getBannedChannel(name)
|
||||
);
|
||||
require('./routes/index')(app, channelIndex, webConfig.getMaxIndexEntries());
|
||||
require('./routes/socketconfig')(app, clusterClient);
|
||||
require('./routes/contact')(app, webConfig);
|
||||
|
@ -212,6 +218,7 @@ module.exports = {
|
|||
require('./acp').init(app, ioConfig);
|
||||
require('../google2vtt').attach(app);
|
||||
require('./routes/google_drive_userscript')(app);
|
||||
require('./routes/iframe')(app);
|
||||
|
||||
app.use(serveStatic(path.join(__dirname, '..', '..', 'www'), {
|
||||
maxAge: webConfig.getCacheTTL()
|
||||
|
|
|
@ -24,7 +24,7 @@ block content
|
|||
tbody
|
||||
for c in channels
|
||||
tr
|
||||
th
|
||||
td
|
||||
form.form-inline.pull-right(action="/account/channels", method="post", onsubmit="return confirm('Are you sure you want to delete " +c.name+ "? This cannot be undone');")
|
||||
input(type="hidden", name="_csrf", value=csrfToken)
|
||||
input(type="hidden", name="action", value="delete_channel")
|
||||
|
@ -32,6 +32,9 @@ block content
|
|||
button.btn.btn-xs.btn-danger(type="submit") Delete
|
||||
span.glyphicon.glyphicon-trash
|
||||
a(href=`/${channelPath}/${c.name}`, style="margin-left: 5px")= c.name
|
||||
if c.banReason != null
|
||||
|
|
||||
span.label.label-danger Banned
|
||||
.col-lg-6.col-md-6
|
||||
h3 Register a new channel
|
||||
if newChannelError
|
||||
|
|
9
templates/banned_channel.pug
Normal file
9
templates/banned_channel.pug
Normal file
|
@ -0,0 +1,9 @@
|
|||
extends layout.pug
|
||||
|
||||
block content
|
||||
.col-md-12
|
||||
.alert.alert-danger
|
||||
h1 Banned Channel
|
||||
strong This channel is banned:
|
||||
p
|
||||
= externalReason
|
|
@ -46,7 +46,7 @@ html(lang="en")
|
|||
#userlist
|
||||
#messagebuffer.linewrap
|
||||
form(action="javascript:void(0)")
|
||||
input#chatline.form-control(type="text", maxlength="320", style="display: none")
|
||||
input#chatline.form-control(type="text", maxlength=maxMsgLen, style="display: none")
|
||||
#guestlogin.input-group
|
||||
span.input-group-addon Guest login
|
||||
input#guestname.form-control(type="text", placeholder="Name")
|
||||
|
@ -63,10 +63,10 @@ html(lang="en")
|
|||
button#emotelistbtn.btn.btn-sm.btn-default Emote List
|
||||
#rightcontrols.col-lg-7.col-md-7
|
||||
#plcontrol.btn-group
|
||||
button#showsearch.btn.btn-sm.btn-default(title="Search for a video", data-toggle="collapse", data-target="#searchcontrol")
|
||||
span.glyphicon.glyphicon-search
|
||||
button#showmediaurl.btn.btn-sm.btn-default(title="Add video from URL", data-toggle="collapse", data-target="#addfromurl")
|
||||
span.glyphicon.glyphicon-plus
|
||||
button#showsearch.btn.btn-sm.btn-default(title="Search for a video", data-toggle="collapse", data-target="#searchcontrol")
|
||||
span.glyphicon.glyphicon-search
|
||||
button#showcustomembed.btn.btn-sm.btn-default(title="Embed a custom frame", data-toggle="collapse", data-target="#customembed")
|
||||
span.glyphicon.glyphicon-th-large
|
||||
button#showplaylistmanager.btn.btn-sm.btn-default(title="Manage playlists", data-toggle="collapse", data-target="#playlistmanager")
|
||||
|
@ -249,14 +249,18 @@ html(lang="en")
|
|||
script(src="/js/paginator.js")
|
||||
script(src="/js/ui.js")
|
||||
script(src="/js/callbacks.js")
|
||||
script(defer, src="/js/vjs/dash.all.min.js")
|
||||
script(defer, src="/js/vjs/video.js")
|
||||
script(defer, src="/js/vjs/videojs-dash.js")
|
||||
script(defer, src="/js/vjs/videojs-hlsjs-plugin.js")
|
||||
script(defer, src="/js/vjs/videojs-resolution-switcher.js")
|
||||
script(defer, src="/js/vjs/videojs-audio-switcher.js")
|
||||
script(defer, src="/js/octopus/subtitles-octopus.js")
|
||||
script(defer, src="/js/playerjs-0.0.12.js")
|
||||
script(defer, src="/js/niconico.js")
|
||||
script(defer, src="/js/peertube.js")
|
||||
script(defer, src="/js/sc.js")
|
||||
script(defer, src="https://www.youtube.com/iframe_api")
|
||||
script(defer, src="https://api.dmcdn.net/all.js")
|
||||
script(defer, src="https://player.vimeo.com/api/player.js")
|
||||
script(defer, src="/js/sc.js")
|
||||
script(defer, src="/js/video.js")
|
||||
script(defer, src="/js/videojs-contrib-hls.min.js")
|
||||
script(defer, src="/js/videojs-resolution-switcher.js")
|
||||
script(defer, src="/js/playerjs-0.0.12.js")
|
||||
script(defer, src="/js/dash.all.min.js")
|
||||
script(defer, src="/js/videojs-dash.js")
|
||||
script(defer, src="https://player.twitch.tv/js/embed/v1.js")
|
||||
|
|
|
@ -3,7 +3,7 @@ mixin footer
|
|||
.container
|
||||
p.text-muted.credit.
|
||||
Powered by CyTube, available on <a href="https://github.com/calzoneman/sync" target="_blank" rel="noreferrer noopener">GitHub</a> · <a href="/contact" target="_blank">Contact</a> · <a href="https://github.com/calzoneman/sync/wiki" target="_blank" rel="noopener noreferrer">Wiki</a>
|
||||
script(src="/js/jquery-1.11.0.min.js")
|
||||
script(src="/js/jquery-1.12.4.min.js")
|
||||
// Must be included before jQuery-UI since jQuery-UI overrides jQuery.fn.button
|
||||
// I should really abandon this crap one day
|
||||
script(src="/js/jquery-ui.js")
|
||||
|
|
36
templates/iframe.pug
Normal file
36
templates/iframe.pug
Normal file
|
@ -0,0 +1,36 @@
|
|||
doctype html
|
||||
html(lang="en")
|
||||
head
|
||||
meta(charset="utf-8")
|
||||
meta(name="viewport", content="width=device-width, initial-scale=1.0")
|
||||
meta(name="referrer", content="same-origin")
|
||||
link(rel="stylesheet", href="/css/video-js.css")
|
||||
link(rel="stylesheet", href="/css/videojs-resolution-switcher.css")
|
||||
style.
|
||||
body { overflow-y: hidden }
|
||||
body
|
||||
#wrap
|
||||
#videowrap
|
||||
#ytapiplayer
|
||||
script.
|
||||
const USEROPTS = {
|
||||
default_quality: 'auto'
|
||||
}
|
||||
const CLIENT = {
|
||||
leader: false
|
||||
}
|
||||
let VOLUME = 0;
|
||||
function waitUntilDefined(obj, key, fn) {
|
||||
if(typeof obj[key] === "undefined") {
|
||||
setTimeout(function () {
|
||||
waitUntilDefined(obj, key, fn);
|
||||
}, 100);
|
||||
return;
|
||||
}
|
||||
fn();
|
||||
}
|
||||
script(src="/js/jquery-1.12.4.min.js")
|
||||
script(src="/js/vjs/video.js")
|
||||
script(src="/js/vjs/videojs-resolution-switcher.js")
|
||||
script(src="/js/playerjs-0.0.12.js")
|
||||
script(src="/js/player.js")
|
|
@ -21,8 +21,15 @@ block content
|
|||
|
||||
append footer
|
||||
script(type="text/javascript").
|
||||
$("#channelname").keydown(function (ev) {
|
||||
const entrance = document.querySelector('#channelname');
|
||||
entrance.addEventListener('keydown', function (ev) {
|
||||
if (ev.keyCode === 13) {
|
||||
location.href = "/#{channelPath}/" + $("#channelname").val();
|
||||
const channel = `/${CHANNELPATH}/${entrance.value}`;
|
||||
if (ev.shiftKey || ev.ctrlKey) {
|
||||
window.open(channel, '_blank');
|
||||
entrance.value = '';
|
||||
} else {
|
||||
location.href = channel;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -68,11 +68,6 @@ mixin us-playback
|
|||
form.form-horizontal(action="javascript:void(0)")
|
||||
+rcheckbox("us-synch", "Synchronize video playback")
|
||||
+textbox("us-synch-accuracy", "Synch threshold (seconds)", "2")
|
||||
+rcheckbox("us-wmode-transparent", "Set wmode=transparent")
|
||||
.form-group
|
||||
.col-sm-4
|
||||
.col-sm-8
|
||||
p.text-info Setting <code>wmode=transparent</code> allows objects to be displayed above the video player, but may cause performance issues on some systems.
|
||||
+rcheckbox("us-hidevideo", "Remove the video player")
|
||||
+rcheckbox("us-playlistbuttons", "Hide playlist buttons by default")
|
||||
+rcheckbox("us-oldbtns", "Old style playlist buttons")
|
||||
|
@ -91,6 +86,7 @@ mixin us-playback
|
|||
.col-sm-4
|
||||
.col-sm-8
|
||||
p.text-info Due to technical changes on YouTube's side, the CyTube quality preference can no longer be automatically applied on YouTube videos. See <a href="https://github.com/calzoneman/sync/issues/726" rel="noopener noreferer" target="_blank">this GitHub issue</a> for details.
|
||||
+rcheckbox("us-peertube", "Accept PeerTube embeds automatically")
|
||||
|
||||
mixin us-chat
|
||||
#us-chat.tab-pane
|
||||
|
|
|
@ -233,7 +233,8 @@ describe('custom-media', () => {
|
|||
contentType: 'text/vtt',
|
||||
name: 'English Subtitles'
|
||||
}
|
||||
]
|
||||
],
|
||||
thumbnail: 'https://example.com/thumb.jpg',
|
||||
}
|
||||
};
|
||||
});
|
||||
|
@ -321,7 +322,8 @@ describe('custom-media', () => {
|
|||
contentType: 'text/vtt',
|
||||
name: 'English Subtitles'
|
||||
}
|
||||
]
|
||||
],
|
||||
thumbnail: 'https://example.com/thumb.jpg',
|
||||
}
|
||||
};
|
||||
|
||||
|
|
52
test/util/simple-cache.js
Normal file
52
test/util/simple-cache.js
Normal file
|
@ -0,0 +1,52 @@
|
|||
const { SimpleCache } = require('../../lib/util/simple-cache');
|
||||
const assert = require('assert');
|
||||
|
||||
describe('SimpleCache', () => {
|
||||
const CACHE_MAX_ELEM = 5;
|
||||
const CACHE_MAX_AGE = 5;
|
||||
let cache;
|
||||
|
||||
beforeEach(() => {
|
||||
cache = new SimpleCache({
|
||||
maxElem: CACHE_MAX_ELEM,
|
||||
maxAge: CACHE_MAX_AGE
|
||||
});
|
||||
});
|
||||
|
||||
it('sets, gets, and deletes a value', () => {
|
||||
assert.strictEqual(cache.get('foo'), null);
|
||||
|
||||
cache.put('foo', 'bar');
|
||||
assert.strictEqual(cache.get('foo'), 'bar');
|
||||
|
||||
cache.delete('foo');
|
||||
assert.strictEqual(cache.get('foo'), null);
|
||||
});
|
||||
|
||||
it('does not return an expired value', done => {
|
||||
cache.put('foo', 'bar');
|
||||
|
||||
setTimeout(() => {
|
||||
assert.strictEqual(cache.get('foo'), null);
|
||||
done();
|
||||
}, CACHE_MAX_AGE + 1);
|
||||
});
|
||||
|
||||
it('cleans up old values', done => {
|
||||
cache.put('foo', 'bar');
|
||||
|
||||
setTimeout(() => {
|
||||
assert.strictEqual(cache.get('foo'), null);
|
||||
done();
|
||||
}, CACHE_MAX_AGE * 2);
|
||||
});
|
||||
|
||||
it('removes the oldest entry if max elem is reached', () => {
|
||||
for (let i = 0; i < CACHE_MAX_ELEM + 1; i++) {
|
||||
cache.put(`foo${i}`, 'bar');
|
||||
}
|
||||
|
||||
assert.strictEqual(cache.get('foo0'), null);
|
||||
assert.strictEqual(cache.get('foo1'), 'bar');
|
||||
});
|
||||
});
|
59
www/.eslintrc.json
Normal file
59
www/.eslintrc.json
Normal file
|
@ -0,0 +1,59 @@
|
|||
{
|
||||
"env": { "browser": true, "jquery": true },
|
||||
"globals": {
|
||||
"CHANNEL": "writable",
|
||||
"CHANNELNAME": "writable",
|
||||
"CHATHIST": "writable",
|
||||
"CHATHISTIDX": "writable",
|
||||
"CHATSOUND": "writable",
|
||||
"CHATTHROTTLE": "writable",
|
||||
"CLIENT": "writable",
|
||||
"CSEMOTELIST": "writable",
|
||||
"DEFAULT_THEME": "writable",
|
||||
"EMOTELIST": "writable",
|
||||
"EMOTELISTMODAL": "writable",
|
||||
"FILTER_FROM": "writable",
|
||||
"FILTER_TO": "writable",
|
||||
"FOCUSED": "writable",
|
||||
"GS_VERSION": "writable",
|
||||
"HAS_CONNECTED_BEFORE": "writable",
|
||||
"IGNORE_SCROLL_EVENT": "writable",
|
||||
"IGNORED": "writable",
|
||||
"IMAGE_MATCH": "writable",
|
||||
"JSPREF": "writable",
|
||||
"KICKED": "writable",
|
||||
"LASTCHAT": "writable",
|
||||
"LEADTMR": "writable",
|
||||
"PAGETITLE": "writable",
|
||||
"PL_ACTION_QUEUE": "writable",
|
||||
"PL_AFTER": "writable",
|
||||
"PL_CURRENT": "writable",
|
||||
"PL_FROM": "writable",
|
||||
"PL_QUEUED_ACTIONS": "writable",
|
||||
"PL_WAIT_SCROLL": "writable",
|
||||
"PLAYER": "writable",
|
||||
"REBUILDING": "writable",
|
||||
"SCROLLCHAT": "writable",
|
||||
"SOCKETIO_CONNECT_ERROR_COUNT": "writable",
|
||||
"SUPERADMIN": "writable",
|
||||
"TITLE_BLINK": "writable",
|
||||
"USEROPTS": "writable",
|
||||
"VHEIGHT": "writable",
|
||||
"VOLUME": "writable",
|
||||
"VWIDTH": "writable",
|
||||
"CyTube": "writable",
|
||||
"Rank": "writable",
|
||||
"getOpt": "writable",
|
||||
"setOpt": "writable",
|
||||
"socket": "writable"
|
||||
},
|
||||
"plugins": [
|
||||
"no-jquery"
|
||||
],
|
||||
"extends": [
|
||||
"plugin:no-jquery/deprecated"
|
||||
],
|
||||
"rules": {
|
||||
"no-jquery/no-event-shorthand": "error"
|
||||
}
|
||||
}
|
Binary file not shown.
1644
www/css/video-js.css
1644
www/css/video-js.css
File diff suppressed because one or more lines are too long
|
@ -1,4 +1,4 @@
|
|||
.vjs-resolution-button .vjs-menu-icon:before {
|
||||
.vjs-resolution-button .vjs-icon-placeholder:before {
|
||||
content: '\f110';
|
||||
font-family: VideoJS;
|
||||
font-weight: normal;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
Callbacks = {
|
||||
const Callbacks = {
|
||||
/* fired when socket connection completes */
|
||||
connect: function() {
|
||||
HAS_CONNECTED_BEFORE = true;
|
||||
|
@ -82,7 +82,7 @@ Callbacks = {
|
|||
var announcement = makeAlert(data.title, data.text + signature)
|
||||
.appendTo($("#announcements"));
|
||||
if (data.id) {
|
||||
announcement.find(".close").click(function suppressThisAnnouncement() {
|
||||
announcement.find(".close").on('click', function suppressThisAnnouncement() {
|
||||
CyTube.ui.suppressedAnnouncementId = data.id;
|
||||
setOpt("suppressed_announcement_id", data.id);
|
||||
});
|
||||
|
@ -179,7 +179,7 @@ Callbacks = {
|
|||
|
||||
$("<button/>").addClass("close pull-right")
|
||||
.appendTo(div)
|
||||
.click(function () {
|
||||
.on('click', function () {
|
||||
div.parent().remove();
|
||||
})
|
||||
.html("×");
|
||||
|
@ -444,7 +444,7 @@ Callbacks = {
|
|||
var li = $("<li/>").appendTo(menu);
|
||||
$("<a/>").attr("href", "javascript:void(0)")
|
||||
.html(disp)
|
||||
.click(function() {
|
||||
.on('click', function() {
|
||||
socket.emit("borrow-rank", r);
|
||||
})
|
||||
.appendTo(li);
|
||||
|
@ -658,8 +658,7 @@ Callbacks = {
|
|||
}
|
||||
$("#drinkcount").text(text);
|
||||
$("#drinkbar").show();
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
$("#drinkbar").hide();
|
||||
}
|
||||
},
|
||||
|
@ -752,8 +751,7 @@ Callbacks = {
|
|||
if(data.temp) {
|
||||
btn.html(btn.html().replace("Make Temporary",
|
||||
"Make Permanent"));
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
btn.html(btn.html().replace("Make Permanent",
|
||||
"Make Temporary"));
|
||||
}
|
||||
|
@ -867,8 +865,7 @@ Callbacks = {
|
|||
$("#qlockbtn").find("span")
|
||||
.removeClass("glyphicon-lock")
|
||||
.addClass("glyphicon-ok");
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
$("#qlockbtn").removeClass("btn-success")
|
||||
.addClass("btn-danger")
|
||||
.attr("title", "Playlist Locked");
|
||||
|
@ -886,7 +883,7 @@ Callbacks = {
|
|||
.css("margin-left", "0")
|
||||
.attr("id", "search_clear")
|
||||
.text("Clear Results")
|
||||
.click(function() {
|
||||
.on('click', function() {
|
||||
clearSearchResults();
|
||||
})
|
||||
.insertBefore($("#library"));
|
||||
|
@ -927,12 +924,12 @@ Callbacks = {
|
|||
var poll = $("<div/>").addClass("well active").prependTo($("#pollwrap"));
|
||||
$("<button/>").addClass("close pull-right").html("×")
|
||||
.appendTo(poll)
|
||||
.click(function() { poll.remove(); });
|
||||
.on('click', function() { poll.remove(); });
|
||||
if(hasPermission("pollctl")) {
|
||||
$("<button/>").addClass("btn btn-danger btn-sm pull-right").text("End Poll")
|
||||
.appendTo(poll)
|
||||
.click(function() {
|
||||
socket.emit("closePoll")
|
||||
.on('click', function() {
|
||||
socket.emit("closePoll");
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -949,11 +946,11 @@ Callbacks = {
|
|||
});
|
||||
$(this).addClass("active");
|
||||
$(this).parent().addClass("option-selected");
|
||||
}
|
||||
};
|
||||
$("<button/>").addClass("btn btn-default btn-sm").text(data.counts[i])
|
||||
.prependTo($("<div/>").addClass("option").html(data.options[i])
|
||||
.appendTo(poll))
|
||||
.click(callback);
|
||||
.on('click', callback);
|
||||
})(i);
|
||||
|
||||
}
|
||||
|
@ -981,7 +978,7 @@ Callbacks = {
|
|||
$(this).attr("disabled", true);
|
||||
});
|
||||
poll.find(".btn-danger").each(function() {
|
||||
$(this).remove()
|
||||
$(this).remove();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
@ -1000,14 +997,14 @@ Callbacks = {
|
|||
updateEmote: function (data) {
|
||||
data.regex = new RegExp(data.source, "gi");
|
||||
var found = false;
|
||||
for (var i = 0; i < CHANNEL.emotes.length; i++) {
|
||||
for (let i = 0; i < CHANNEL.emotes.length; i++) {
|
||||
if (CHANNEL.emotes[i].name === data.name) {
|
||||
found = true;
|
||||
CHANNEL.emotes[i] = data;
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (var i = 0; i < CHANNEL.badEmotes.length; i++) {
|
||||
for (let i = 0; i < CHANNEL.badEmotes.length; i++) {
|
||||
if (CHANNEL.badEmotes[i].name === data.name) {
|
||||
CHANNEL.badEmotes[i] = data;
|
||||
break;
|
||||
|
@ -1050,22 +1047,20 @@ Callbacks = {
|
|||
if(!badBefore){
|
||||
CHANNEL.badEmotes.push(data);
|
||||
delete CHANNEL.emoteMap[oldName];
|
||||
}
|
||||
// Was bad before too: Update
|
||||
else {
|
||||
for (var i = 0; i < CHANNEL.badEmotes.length; i++) {
|
||||
} else {
|
||||
for (let i = 0; i < CHANNEL.badEmotes.length; i++) {
|
||||
if (CHANNEL.badEmotes[i].name === oldName) {
|
||||
CHANNEL.badEmotes[i] = data;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Not bad now
|
||||
else {
|
||||
} else {
|
||||
// But was bad before: Drop from list
|
||||
if(badBefore){
|
||||
for (var i = 0; i < CHANNEL.badEmotes.length; i++) {
|
||||
for (let i = 0; i < CHANNEL.badEmotes.length; i++) {
|
||||
if (CHANNEL.badEmotes[i].name === oldName) {
|
||||
CHANNEL.badEmotes.splice(i, 1);
|
||||
break;
|
||||
|
@ -1083,7 +1078,7 @@ Callbacks = {
|
|||
|
||||
removeEmote: function (data) {
|
||||
var found = -1;
|
||||
for (var i = 0; i < CHANNEL.emotes.length; i++) {
|
||||
for (let i = 0; i < CHANNEL.emotes.length; i++) {
|
||||
if (CHANNEL.emotes[i].name === data.name) {
|
||||
found = i;
|
||||
break;
|
||||
|
@ -1093,9 +1088,9 @@ Callbacks = {
|
|||
if (found !== -1) {
|
||||
var row = $("code:contains('" + data.name + "')").parent().parent();
|
||||
row.hide("fade", row.remove.bind(row));
|
||||
CHANNEL.emotes.splice(i, 1);
|
||||
CHANNEL.emotes.splice(found, 1);
|
||||
delete CHANNEL.emoteMap[data.name];
|
||||
for (var i = 0; i < CHANNEL.badEmotes.length; i++) {
|
||||
for (let i = 0; i < CHANNEL.badEmotes.length; i++) {
|
||||
if (CHANNEL.badEmotes[i].name === data.name) {
|
||||
CHANNEL.badEmotes.splice(i, 1);
|
||||
break;
|
||||
|
@ -1171,20 +1166,31 @@ Callbacks = {
|
|||
$("#voteskip").attr("disabled", false);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var SOCKET_DEBUG = localStorage.getItem('cytube_socket_debug') === 'true';
|
||||
setupCallbacks = function() {
|
||||
window.Callbacks = Callbacks;
|
||||
|
||||
// For sanity, do this
|
||||
// localStorage.setItem('cytube_socket_omissions', '["mediaUpdate"]')
|
||||
var SOCKET_DEBUG = {
|
||||
enabled: (localStorage.getItem('cytube_socket_debug') === 'true'),
|
||||
omit: (((data)=>{
|
||||
const frames = data === null ? [] : JSON.parse(data);
|
||||
return frames;
|
||||
})(localStorage.getItem('cytube_socket_omissions')))
|
||||
};
|
||||
|
||||
function setupCallbacks() {
|
||||
for(var key in Callbacks) {
|
||||
(function(key) {
|
||||
socket.on(key, function(data) {
|
||||
if (SOCKET_DEBUG) {
|
||||
if (SOCKET_DEBUG.enabled && !SOCKET_DEBUG.omit.includes(key)) {
|
||||
console.log(key, data);
|
||||
}
|
||||
try {
|
||||
Callbacks[key](data);
|
||||
} catch (e) {
|
||||
if (SOCKET_DEBUG) {
|
||||
if (SOCKET_DEBUG.enabled) {
|
||||
console.log("EXCEPTION: " + e + "\n" + e.stack);
|
||||
}
|
||||
}
|
||||
|
@ -1211,7 +1217,7 @@ setupCallbacks = function() {
|
|||
.appendTo($("#announcements"));
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function ioServerConnect(socketConfig) {
|
||||
if (socketConfig.error) {
|
||||
|
@ -1293,7 +1299,7 @@ function initSocketIO(socketConfig) {
|
|||
|
||||
function checkLetsEncrypt(socketConfig, nonLetsEncryptError) {
|
||||
var servers = socketConfig.servers.filter(function (server) {
|
||||
return !server.secure && !server.ipv6Only
|
||||
return !server.secure && !server.ipv6Only;
|
||||
});
|
||||
|
||||
if (servers.length === 0) {
|
||||
|
|
30
www/js/dash.all.min.js
vendored
30
www/js/dash.all.min.js
vendored
File diff suppressed because one or more lines are too long
|
@ -1,3 +1,55 @@
|
|||
/*eslint no-unused-vars: "off"*/
|
||||
|
||||
(function() {
|
||||
/**
|
||||
* Test whether the browser supports nullish-coalescing operator.
|
||||
*
|
||||
* Users with old browsers will probably fail to load the client correctly
|
||||
* because parsing this operator in older browsers results in a SyntaxError
|
||||
* that aborts compilation of the entire script (not just an exception where
|
||||
* it is used). In particular, as of 2023-01-28, Utherverse ships with
|
||||
* a rather old browser version (Chrome 76) and several users have reported
|
||||
* it not working.
|
||||
*/
|
||||
try {
|
||||
try {
|
||||
new Function('x?.y');
|
||||
} catch (e) {
|
||||
if (e.name === 'SyntaxError') {
|
||||
/**
|
||||
* If we're at this point, we can't be sure what scripts have
|
||||
* actually loaded, so construct the error alert the old
|
||||
* fashioned way.
|
||||
*/
|
||||
var wrap = document.createElement('div');
|
||||
wrap.className = 'col-md-12';
|
||||
var al = document.createElement('div');
|
||||
al.className = 'alert alert-danger';
|
||||
var title = document.createElement('strong');
|
||||
title.textContent = 'Unsupported Browser';
|
||||
var msg = document.createElement('p');
|
||||
msg.textContent = 'It looks like your browser does not support ' +
|
||||
'the required JavaScript features to run ' +
|
||||
'CyTube. This is usually caused by ' +
|
||||
'using an outdated browser version. Please '+
|
||||
'check if an update is available. Your ' +
|
||||
'browser version is reported as:';
|
||||
var version = document.createElement('tt');
|
||||
version.textContent = navigator.userAgent;
|
||||
|
||||
wrap.appendChild(al);
|
||||
al.appendChild(title);
|
||||
al.appendChild(msg);
|
||||
al.appendChild(document.createElement('br'));
|
||||
al.appendChild(version);
|
||||
document.getElementById('motdrow').appendChild(wrap);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error probing for feature support:', e.stack);
|
||||
}
|
||||
})();
|
||||
|
||||
var CL_VERSION = 3.0;
|
||||
var GS_VERSION = 1.7; // Google Drive Userscript
|
||||
|
||||
|
@ -48,7 +100,8 @@ var CHATMAXSIZE = 100;
|
|||
var SCROLLCHAT = true;
|
||||
var IGNORE_SCROLL_EVENT = false;
|
||||
var LASTCHAT = {
|
||||
name: ""
|
||||
name: "",
|
||||
time: 0
|
||||
};
|
||||
var FOCUSED = true;
|
||||
var PAGETITLE = "CyTube";
|
||||
|
@ -112,33 +165,37 @@ function getOrDefault(k, def) {
|
|||
var IGNORED = getOrDefault("ignorelist", []);
|
||||
|
||||
var USEROPTS = {
|
||||
// General tab
|
||||
theme : getOrDefault("theme", DEFAULT_THEME), // Set in head template
|
||||
layout : getOrDefault("layout", "fluid"),
|
||||
synch : getOrDefault("synch", true),
|
||||
hidevid : getOrDefault("hidevid", false),
|
||||
show_timestamps : getOrDefault("show_timestamps", true),
|
||||
modhat : getOrDefault("modhat", false),
|
||||
blink_title : getOrDefault("blink_title", "onlyping"),
|
||||
sync_accuracy : getOrDefault("sync_accuracy", 2),
|
||||
wmode_transparent : getOrDefault("wmode_transparent", true),
|
||||
chatbtn : getOrDefault("chatbtn", false),
|
||||
altsocket : getOrDefault("altsocket", false),
|
||||
qbtn_hide : getOrDefault("qbtn_hide", false),
|
||||
qbtn_idontlikechange : getOrDefault("qbtn_idontlikechange", false),
|
||||
first_visit : getOrDefault("first_visit", true),
|
||||
ignore_channelcss : getOrDefault("ignore_channelcss", false),
|
||||
ignore_channeljs : getOrDefault("ignore_channeljs", false),
|
||||
// Playback tab
|
||||
synch : getOrDefault("synch", true),
|
||||
sync_accuracy : getOrDefault("sync_accuracy", 2),
|
||||
hidevid : getOrDefault("hidevid", false),
|
||||
default_quality : getOrDefault("default_quality", "auto"),
|
||||
qbtn_hide : getOrDefault("qbtn_hide", false),
|
||||
qbtn_idontlikechange : getOrDefault("qbtn_idontlikechange", false),
|
||||
peertube_risk : getOrDefault("peertube_risk", false),
|
||||
// Chat tab
|
||||
show_timestamps : getOrDefault("show_timestamps", true),
|
||||
sort_rank : getOrDefault("sort_rank", true),
|
||||
sort_afk : getOrDefault("sort_afk", false),
|
||||
default_quality : getOrDefault("default_quality", "auto"),
|
||||
blink_title : getOrDefault("blink_title", "onlyping"),
|
||||
boop : getOrDefault("boop", "never"),
|
||||
show_shadowchat : getOrDefault("show_shadowchat", false),
|
||||
emotelist_sort : getOrDefault("emotelist_sort", true),
|
||||
notifications : getOrDefault("notifications", "never"),
|
||||
chatbtn : getOrDefault("chatbtn", false),
|
||||
no_emotes : getOrDefault("no_emotes", false),
|
||||
strip_image : getOrDefault("strip_image", false),
|
||||
chat_tab_method : getOrDefault("chat_tab_method", "Cycle options"),
|
||||
notifications : getOrDefault("notifications", "never"),
|
||||
show_ip_in_tooltip : getOrDefault("show_ip_in_tooltip", true)
|
||||
// Moderator tab
|
||||
modhat : getOrDefault("modhat", false),
|
||||
show_shadowchat : getOrDefault("show_shadowchat", false),
|
||||
show_ip_in_tooltip : getOrDefault("show_ip_in_tooltip", true),
|
||||
// Elsewhere
|
||||
first_visit : getOrDefault("first_visit", true),
|
||||
emotelist_sort : getOrDefault("emotelist_sort", true),
|
||||
};
|
||||
|
||||
/* Backwards compatibility check */
|
||||
|
@ -179,7 +236,6 @@ if (["never", "onlyping", "always"].indexOf(USEROPTS.boop) === -1) {
|
|||
|
||||
var VOLUME = parseFloat(getOrDefault("volume", 1));
|
||||
|
||||
var NO_WEBSOCKETS = USEROPTS.altsocket;
|
||||
var NO_VIMEO = Boolean(location.host.match("cytu.be"));
|
||||
|
||||
var JSPREF = getOpt("channel_js_pref") || {};
|
||||
|
|
4
www/js/jquery-1.11.0.min.js
vendored
4
www/js/jquery-1.11.0.min.js
vendored
File diff suppressed because one or more lines are too long
5
www/js/jquery-1.12.4.min.js
vendored
Normal file
5
www/js/jquery-1.12.4.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
22169
www/js/jquery-ui.js
vendored
22169
www/js/jquery-ui.js
vendored
File diff suppressed because it is too large
Load diff
237
www/js/niconico.js
Normal file
237
www/js/niconico.js
Normal file
|
@ -0,0 +1,237 @@
|
|||
/*
|
||||
* Niconico iframe embed api
|
||||
* Written by Xaekai
|
||||
* Copyright (c) 2022 Radiant Feather; Licensed AGPLv3
|
||||
*
|
||||
* Dual-licensed MIT when distributed with CyTube/sync.
|
||||
*
|
||||
*/
|
||||
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,
|
||||
autoplay: 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: 'mute', 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: 'volumeChange', data: { volume } });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
window.NicovideoEmbed = NicovideoEmbed;
|
BIN
www/js/octopus/subtitles-octopus-worker-legacy.data
Normal file
BIN
www/js/octopus/subtitles-octopus-worker-legacy.data
Normal file
Binary file not shown.
35
www/js/octopus/subtitles-octopus-worker-legacy.js
Normal file
35
www/js/octopus/subtitles-octopus-worker-legacy.js
Normal file
File diff suppressed because one or more lines are too long
BIN
www/js/octopus/subtitles-octopus-worker-legacy.js.mem
Normal file
BIN
www/js/octopus/subtitles-octopus-worker-legacy.js.mem
Normal file
Binary file not shown.
BIN
www/js/octopus/subtitles-octopus-worker.data
Normal file
BIN
www/js/octopus/subtitles-octopus-worker.data
Normal file
Binary file not shown.
1
www/js/octopus/subtitles-octopus-worker.js
Normal file
1
www/js/octopus/subtitles-octopus-worker.js
Normal file
File diff suppressed because one or more lines are too long
BIN
www/js/octopus/subtitles-octopus-worker.wasm
Executable file
BIN
www/js/octopus/subtitles-octopus-worker.wasm
Executable file
Binary file not shown.
1547
www/js/octopus/subtitles-octopus.js
Normal file
1547
www/js/octopus/subtitles-octopus.js
Normal file
File diff suppressed because it is too large
Load diff
|
@ -31,10 +31,10 @@
|
|||
s = s + this.opts.maxPages < pages ? s : pages - this.opts.maxPages;
|
||||
s = s < 0 ? 0 : s;
|
||||
if(endcaps) {
|
||||
var li = $("<li/>").appendTo(ul);
|
||||
let li = $("<li/>").appendTo(ul);
|
||||
$("<a/>").attr("href", "javascript:void(0)")
|
||||
.html("«")
|
||||
.click(function () {
|
||||
.on('click', function () {
|
||||
this.loadPage(0);
|
||||
}.bind(this))
|
||||
.appendTo(li);
|
||||
|
@ -43,7 +43,7 @@
|
|||
li.addClass("disabled");
|
||||
|
||||
if(s > 0) {
|
||||
var sep = $("<li/>").addClass("disabled")
|
||||
let sep = $("<li/>").addClass("disabled")
|
||||
.appendTo(ul);
|
||||
$("<a/>").attr("href", "javascript:void(0)")
|
||||
.html("…")
|
||||
|
@ -52,12 +52,12 @@
|
|||
}
|
||||
for(var i = s; i < s + this.opts.maxPages && i < s + pages; i++) {
|
||||
(function (i) {
|
||||
var li = $("<li/>").appendTo(ul);
|
||||
let li = $("<li/>").appendTo(ul);
|
||||
if(i == p)
|
||||
li.addClass("active");
|
||||
$("<a/>").attr("href", "javascript:void(0)")
|
||||
.text(i + 1)
|
||||
.click(function () {
|
||||
.on('click', function () {
|
||||
this.loadPage(i);
|
||||
}.bind(this))
|
||||
.appendTo(li);
|
||||
|
@ -65,17 +65,17 @@
|
|||
}
|
||||
if(endcaps) {
|
||||
if(s + this.opts.maxPages < pages) {
|
||||
var sep = $("<li/>").addClass("disabled")
|
||||
let sep = $("<li/>").addClass("disabled")
|
||||
.appendTo(ul);
|
||||
$("<a/>").attr("href", "javascript:void(0)")
|
||||
.html("…")
|
||||
.appendTo(sep);
|
||||
}
|
||||
|
||||
var li = $("<li/>").appendTo(ul);
|
||||
let li = $("<li/>").appendTo(ul);
|
||||
$("<a/>").attr("href", "javascript:void(0)")
|
||||
.html("»")
|
||||
.click(function () {
|
||||
.on('click', function () {
|
||||
this.loadPage(pages - 1);
|
||||
}.bind(this))
|
||||
.appendTo(li);
|
||||
|
|
1
www/js/peertube.js
Normal file
1
www/js/peertube.js
Normal file
File diff suppressed because one or more lines are too long
128
www/js/ui.js
128
www/js/ui.js
|
@ -10,11 +10,11 @@ CyTube.ui.onPageBlur = function (event) {
|
|||
FOCUSED = false;
|
||||
};
|
||||
|
||||
$(window).focus(CyTube.ui.onPageFocus).blur(CyTube.ui.onPageBlur);
|
||||
$(window).on('focus', CyTube.ui.onPageFocus).on('blur', CyTube.ui.onPageBlur);
|
||||
// See #783
|
||||
$(".modal").focus(CyTube.ui.onPageFocus);
|
||||
$(".modal").on('focus', CyTube.ui.onPageFocus);
|
||||
|
||||
$("#togglemotd").click(function () {
|
||||
$("#togglemotd").on('click', function () {
|
||||
var hidden = $("#motd")[0].style.display === "none";
|
||||
$("#motd").toggle();
|
||||
if (hidden) {
|
||||
|
@ -30,7 +30,7 @@ $("#togglemotd").click(function () {
|
|||
|
||||
/* chatbox */
|
||||
|
||||
$("#modflair").click(function () {
|
||||
$("#modflair").on('click', function () {
|
||||
var m = $("#modflair");
|
||||
if (m.hasClass("label-success")) {
|
||||
USEROPTS.modhat = false;
|
||||
|
@ -54,7 +54,7 @@ $("#modflair").click(function () {
|
|||
setOpt('modhat', USEROPTS.modhat);
|
||||
});
|
||||
|
||||
$("#usercount").mouseenter(function (ev) {
|
||||
$("#usercount").on('mouseenter', function (ev) {
|
||||
var breakdown = calcUserBreakdown();
|
||||
// re-using profile-box class for convenience
|
||||
var popup = $("<div/>")
|
||||
|
@ -72,7 +72,7 @@ $("#usercount").mouseenter(function (ev) {
|
|||
popup.html(contents);
|
||||
});
|
||||
|
||||
$("#usercount").mousemove(function (ev) {
|
||||
$("#usercount").on('mousemove', function (ev) {
|
||||
var popup = $("#usercount").find(".profile-box");
|
||||
if(popup.length == 0)
|
||||
return;
|
||||
|
@ -81,11 +81,11 @@ $("#usercount").mousemove(function (ev) {
|
|||
popup.css("left", (ev.clientX) + "px");
|
||||
});
|
||||
|
||||
$("#usercount").mouseleave(function () {
|
||||
$("#usercount").on('mouseleave', function () {
|
||||
$("#usercount").find(".profile-box").remove();
|
||||
});
|
||||
|
||||
$("#messagebuffer").scroll(function (ev) {
|
||||
$("#messagebuffer").on('scroll', function (ev) {
|
||||
if (IGNORE_SCROLL_EVENT) {
|
||||
// Skip event, this was triggered by scrollChat() and not by a user action.
|
||||
// Reset for next event.
|
||||
|
@ -109,7 +109,7 @@ $("#messagebuffer").scroll(function (ev) {
|
|||
}
|
||||
});
|
||||
|
||||
$("#guestname").keydown(function (ev) {
|
||||
$("#guestname").on('keydown', function (ev) {
|
||||
if (ev.keyCode === 13) {
|
||||
socket.emit("login", {
|
||||
name: $("#guestname").val()
|
||||
|
@ -165,7 +165,7 @@ function chatTabComplete(chatline) {
|
|||
chatline.setSelectionRange(result.newPosition, result.newPosition);
|
||||
}
|
||||
|
||||
$("#chatline").keydown(function(ev) {
|
||||
$("#chatline").on('keydown', function(ev) {
|
||||
// Enter/return
|
||||
if(ev.keyCode == 13) {
|
||||
if (CHATTHROTTLE) {
|
||||
|
@ -229,10 +229,10 @@ $("#chatline").keydown(function(ev) {
|
|||
});
|
||||
|
||||
/* poll controls */
|
||||
$("#newpollbtn").click(showPollMenu);
|
||||
$("#newpollbtn").on('click', showPollMenu);
|
||||
|
||||
/* search controls */
|
||||
$("#library_search").click(function() {
|
||||
$("#library_search").on('click', function() {
|
||||
if (!hasPermission("seeplaylist")) {
|
||||
$("#searchcontrol .alert").remove();
|
||||
var al = makeAlert("Permission Denied",
|
||||
|
@ -248,7 +248,7 @@ $("#library_search").click(function() {
|
|||
});
|
||||
});
|
||||
|
||||
$("#library_query").keydown(function(ev) {
|
||||
$("#library_query").on('keydown', function(ev) {
|
||||
if(ev.keyCode == 13) {
|
||||
if (!hasPermission("seeplaylist")) {
|
||||
$("#searchcontrol .alert").remove();
|
||||
|
@ -266,7 +266,7 @@ $("#library_query").keydown(function(ev) {
|
|||
}
|
||||
});
|
||||
|
||||
$("#youtube_search").click(function () {
|
||||
$("#youtube_search").on('click', function () {
|
||||
var query = $("#library_query").val().toLowerCase();
|
||||
try {
|
||||
parseMediaLink(query);
|
||||
|
@ -285,7 +285,7 @@ $("#youtube_search").click(function () {
|
|||
|
||||
/* user playlists */
|
||||
|
||||
$("#userpl_save").click(function() {
|
||||
$("#userpl_save").on('click', function() {
|
||||
if($("#userpl_name").val().trim() == "") {
|
||||
makeAlert("Invalid Name", "Playlist name cannot be empty", "alert-danger")
|
||||
.insertAfter($("#userpl_save").parent());
|
||||
|
@ -298,7 +298,7 @@ $("#userpl_save").click(function() {
|
|||
|
||||
/* video controls */
|
||||
|
||||
$("#mediarefresh").click(function() {
|
||||
$("#mediarefresh").on('click', function() {
|
||||
PLAYER.mediaType = "";
|
||||
PLAYER.mediaId = "";
|
||||
// playerReady triggers the server to send a changeMedia.
|
||||
|
@ -459,12 +459,12 @@ function queue(pos, src) {
|
|||
}
|
||||
}
|
||||
|
||||
$("#queue_next").click(queue.bind(this, "next", "url"));
|
||||
$("#queue_end").click(queue.bind(this, "end", "url"));
|
||||
$("#ce_queue_next").click(queue.bind(this, "next", "customembed"));
|
||||
$("#ce_queue_end").click(queue.bind(this, "end", "customembed"));
|
||||
$("#queue_next").on('click', queue.bind(this, "next", "url"));
|
||||
$("#queue_end").on('click', queue.bind(this, "end", "url"));
|
||||
$("#ce_queue_next").on('click', queue.bind(this, "next", "customembed"));
|
||||
$("#ce_queue_end").on('click', queue.bind(this, "end", "customembed"));
|
||||
|
||||
$("#mediaurl").keyup(function(ev) {
|
||||
$("#mediaurl").on('keyup', function(ev) {
|
||||
if (ev.keyCode === 13) {
|
||||
queue("end", "url");
|
||||
} else {
|
||||
|
@ -487,7 +487,7 @@ $("#mediaurl").keyup(function(ev) {
|
|||
$("<input/>").addClass("form-control")
|
||||
.attr("type", "text")
|
||||
.attr("id", "addfromurl-title-val")
|
||||
.keydown(function (ev) {
|
||||
.on('keydown', function (ev) {
|
||||
if (ev.keyCode === 13) {
|
||||
queue("end", "url");
|
||||
}
|
||||
|
@ -500,22 +500,22 @@ $("#mediaurl").keyup(function(ev) {
|
|||
}
|
||||
});
|
||||
|
||||
$("#customembed-content").keydown(function(ev) {
|
||||
$("#customembed-content").on('keydown', function(ev) {
|
||||
if (ev.keyCode === 13) {
|
||||
queue("end", "customembed");
|
||||
}
|
||||
});
|
||||
|
||||
$("#qlockbtn").click(function() {
|
||||
$("#qlockbtn").on('click', function() {
|
||||
socket.emit("togglePlaylistLock");
|
||||
});
|
||||
|
||||
$("#voteskip").click(function() {
|
||||
$("#voteskip").on('click', function() {
|
||||
socket.emit("voteskip");
|
||||
$("#voteskip").attr("disabled", true);
|
||||
});
|
||||
|
||||
$("#getplaylist").click(function() {
|
||||
$("#getplaylist").on('click', function() {
|
||||
var callback = function(data) {
|
||||
var idx = socket.listeners("errorMsg").indexOf(errCallback);
|
||||
if (idx >= 0) {
|
||||
|
@ -574,14 +574,14 @@ $("#getplaylist").click(function() {
|
|||
socket.emit("requestPlaylist");
|
||||
});
|
||||
|
||||
$("#clearplaylist").click(function() {
|
||||
$("#clearplaylist").on('click', function() {
|
||||
var clear = confirm("Are you sure you want to clear the playlist?");
|
||||
if(clear) {
|
||||
socket.emit("clearPlaylist");
|
||||
}
|
||||
});
|
||||
|
||||
$("#shuffleplaylist").click(function() {
|
||||
$("#shuffleplaylist").on('click', function() {
|
||||
var shuffle = confirm("Are you sure you want to shuffle the playlist?");
|
||||
if(shuffle) {
|
||||
socket.emit("shufflePlaylist");
|
||||
|
@ -596,13 +596,13 @@ function chanrankSubmit(rank) {
|
|||
rank: rank
|
||||
});
|
||||
}
|
||||
$("#cs-chanranks-mod").click(chanrankSubmit.bind(this, 2));
|
||||
$("#cs-chanranks-adm").click(chanrankSubmit.bind(this, 3));
|
||||
$("#cs-chanranks-owner").click(chanrankSubmit.bind(this, 4));
|
||||
$("#cs-chanranks-mod").on('click', chanrankSubmit.bind(this, 2));
|
||||
$("#cs-chanranks-adm").on('click', chanrankSubmit.bind(this, 3));
|
||||
$("#cs-chanranks-owner").on('click', chanrankSubmit.bind(this, 4));
|
||||
|
||||
["#showmediaurl", "#showsearch", "#showcustomembed", "#showplaylistmanager"]
|
||||
.forEach(function (id) {
|
||||
$(id).click(function () {
|
||||
$(id).on('click', function () {
|
||||
var wasActive = $(id).hasClass("active");
|
||||
$(".plcontrol-collapse").collapse("hide");
|
||||
$("#plcontrol button.active").button("toggle");
|
||||
|
@ -616,7 +616,7 @@ $("#plcontrol button").button("hide");
|
|||
$(".plcontrol-collapse").collapse();
|
||||
$(".plcontrol-collapse").collapse("hide");
|
||||
|
||||
$(".cs-checkbox").change(function () {
|
||||
$(".cs-checkbox").on('change', function () {
|
||||
var box = $(this);
|
||||
var key = box.attr("id").replace("cs-", "");
|
||||
var value = box.prop("checked");
|
||||
|
@ -625,7 +625,7 @@ $(".cs-checkbox").change(function () {
|
|||
socket.emit("setOptions", data);
|
||||
});
|
||||
|
||||
$(".cs-textbox").keyup(function () {
|
||||
$(".cs-textbox").on('keyup', function () {
|
||||
var box = $(this);
|
||||
var key = box.attr("id").replace("cs-", "");
|
||||
var value = box.val();
|
||||
|
@ -652,7 +652,7 @@ $(".cs-textbox").keyup(function () {
|
|||
}, 1000);
|
||||
});
|
||||
|
||||
$(".cs-textbox-timeinput").keyup(function (event) {
|
||||
$(".cs-textbox-timeinput").on('keyup', function (event) {
|
||||
var box = $(this);
|
||||
var key = box.attr("id").replace("cs-", "");
|
||||
var value = box.val();
|
||||
|
@ -682,31 +682,31 @@ $(".cs-textbox-timeinput").keyup(function (event) {
|
|||
}, 1000);
|
||||
});
|
||||
|
||||
$("#cs-chanlog-refresh").click(function () {
|
||||
$("#cs-chanlog-refresh").on('click', function () {
|
||||
socket.emit("readChanLog");
|
||||
});
|
||||
|
||||
$("#cs-chanlog-filter").change(filterChannelLog);
|
||||
$("#cs-chanlog-filter").on('change', filterChannelLog);
|
||||
|
||||
$("#cs-motdsubmit").click(function () {
|
||||
$("#cs-motdsubmit").on('click', function () {
|
||||
socket.emit("setMotd", {
|
||||
motd: $("#cs-motdtext").val()
|
||||
});
|
||||
});
|
||||
|
||||
$("#cs-csssubmit").click(function () {
|
||||
$("#cs-csssubmit").on('click', function () {
|
||||
socket.emit("setChannelCSS", {
|
||||
css: $("#cs-csstext").val()
|
||||
});
|
||||
});
|
||||
|
||||
$("#cs-jssubmit").click(function () {
|
||||
$("#cs-jssubmit").on('click', function () {
|
||||
socket.emit("setChannelJS", {
|
||||
js: $("#cs-jstext").val()
|
||||
});
|
||||
});
|
||||
|
||||
$("#cs-chatfilters-newsubmit").click(function () {
|
||||
$("#cs-chatfilters-newsubmit").on('click', function () {
|
||||
var name = $("#cs-chatfilters-newname").val();
|
||||
var regex = $("#cs-chatfilters-newregex").val();
|
||||
var flags = $("#cs-chatfilters-newflags").val();
|
||||
|
@ -735,7 +735,7 @@ $("#cs-chatfilters-newsubmit").click(function () {
|
|||
});
|
||||
});
|
||||
|
||||
$("#cs-emotes-newsubmit").click(function () {
|
||||
$("#cs-emotes-newsubmit").on('click', function () {
|
||||
var name = $("#cs-emotes-newname").val();
|
||||
var image = $("#cs-emotes-newimage").val();
|
||||
|
||||
|
@ -748,7 +748,7 @@ $("#cs-emotes-newsubmit").click(function () {
|
|||
$("#cs-emotes-newimage").val("");
|
||||
});
|
||||
|
||||
$("#cs-chatfilters-export").click(function () {
|
||||
$("#cs-chatfilters-export").on('click', function () {
|
||||
var callback = function (data) {
|
||||
socket.listeners("chatFilters").splice(
|
||||
socket.listeners("chatFilters").indexOf(callback)
|
||||
|
@ -761,7 +761,7 @@ $("#cs-chatfilters-export").click(function () {
|
|||
socket.emit("requestChatFilters");
|
||||
});
|
||||
|
||||
$("#cs-chatfilters-import").click(function () {
|
||||
$("#cs-chatfilters-import").on('click', function () {
|
||||
var text = $("#cs-chatfilters-exporttext").val();
|
||||
var choose = confirm("You are about to import filters from the contents of the textbox below the import button. If this is empty, it will clear all of your filters. Are you sure you want to continue?");
|
||||
if (!choose) {
|
||||
|
@ -783,7 +783,7 @@ $("#cs-chatfilters-import").click(function () {
|
|||
socket.emit("importFilters", data);
|
||||
});
|
||||
|
||||
$("#cs-emotes-export").click(function () {
|
||||
$("#cs-emotes-export").on('click', function () {
|
||||
var em = CHANNEL.emotes.map(function (f) {
|
||||
return {
|
||||
name: f.name,
|
||||
|
@ -793,7 +793,7 @@ $("#cs-emotes-export").click(function () {
|
|||
$("#cs-emotes-exporttext").val(JSON.stringify(em));
|
||||
});
|
||||
|
||||
$("#cs-emotes-import").click(function () {
|
||||
$("#cs-emotes-import").on('click', function () {
|
||||
var text = $("#cs-emotes-exporttext").val();
|
||||
var choose = confirm("You are about to import emotes from the contents of the textbox below the import button. If this is empty, it will clear all of your emotes. Are you sure you want to continue?");
|
||||
if (!choose) {
|
||||
|
@ -827,10 +827,10 @@ var toggleUserlist = function () {
|
|||
scrollChat();
|
||||
};
|
||||
|
||||
$("#usercount").click(toggleUserlist);
|
||||
$("#userlisttoggle").click(toggleUserlist);
|
||||
$("#usercount").on('click', toggleUserlist);
|
||||
$("#userlisttoggle").on('click', toggleUserlist);
|
||||
|
||||
$(".add-temp").change(function () {
|
||||
$(".add-temp").on('change', function () {
|
||||
$(".add-temp").prop("checked", $(this).prop("checked"));
|
||||
});
|
||||
|
||||
|
@ -878,17 +878,18 @@ applyOpts();
|
|||
})();
|
||||
|
||||
var EMOTELISTMODAL = $("#emotelist");
|
||||
$("#emotelistbtn").click(function () {
|
||||
$("#emotelistbtn").on('click', function () {
|
||||
EMOTELISTMODAL.modal();
|
||||
});
|
||||
|
||||
EMOTELISTMODAL.on('shown.bs.modal', function () { $('.emotelist-search').trigger('focus') });
|
||||
EMOTELISTMODAL.find(".emotelist-alphabetical").change(function () {
|
||||
USEROPTS.emotelist_sort = this.checked;
|
||||
setOpt("emotelist_sort", USEROPTS.emotelist_sort);
|
||||
});
|
||||
EMOTELISTMODAL.find(".emotelist-alphabetical").prop("checked", USEROPTS.emotelist_sort);
|
||||
|
||||
$("#fullscreenbtn").click(function () {
|
||||
$("#fullscreenbtn").on('click', function () {
|
||||
var elem = document.querySelector("#videowrap .embed-responsive");
|
||||
// this shit is why frontend web development sucks
|
||||
var fn = elem.requestFullscreen ||
|
||||
|
@ -903,27 +904,30 @@ $("#fullscreenbtn").click(function () {
|
|||
|
||||
function handleCSSJSTooLarge(selector) {
|
||||
if (this.value.length > 20000) {
|
||||
var warning = $(selector);
|
||||
if (warning.length > 0) {
|
||||
let notice = document.querySelector(selector);
|
||||
if (notice !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
warning = makeAlert("Maximum Size Exceeded", "Inline CSS and JavaScript are " +
|
||||
notice = makeAlert("Maximum Size Exceeded", "Inline CSS and JavaScript are " +
|
||||
"limited to 20,000 characters or less. If you need more room, you " +
|
||||
"need to use the external CSS or JavaScript option.", "alert-danger")
|
||||
.attr("id", selector.replace(/#/, ""));
|
||||
warning.insertBefore(this);
|
||||
|
||||
// makeAlert returns jQuery
|
||||
this.parentNode.insertBefore(notice[0], this);
|
||||
} else {
|
||||
$(selector).remove();
|
||||
let notice = document.querySelector(selector);
|
||||
notice?.remove();
|
||||
}
|
||||
}
|
||||
|
||||
$("#cs-csstext").bind("input", handleCSSJSTooLarge.bind($("#cs-csstext")[0],
|
||||
"#cs-csstext-too-big"));
|
||||
$("#cs-jstext").bind("input", handleCSSJSTooLarge.bind($("#cs-jstext")[0],
|
||||
"#cs-jstext-too-big"));
|
||||
['#cs-csstext', '#cs-jstext'].forEach((selector)=>{
|
||||
elem = document.querySelector(selector);
|
||||
elem.addEventListener('input', handleCSSJSTooLarge.bind(elem, `${selector}-too-big`));
|
||||
});
|
||||
|
||||
$("#resize-video-larger").click(function () {
|
||||
$("#resize-video-larger").on('click', function () {
|
||||
try {
|
||||
CyTube.ui.changeVideoWidth(1);
|
||||
} catch (error) {
|
||||
|
@ -931,7 +935,7 @@ $("#resize-video-larger").click(function () {
|
|||
}
|
||||
});
|
||||
|
||||
$("#resize-video-smaller").click(function () {
|
||||
$("#resize-video-smaller").on('click', function () {
|
||||
try {
|
||||
CyTube.ui.changeVideoWidth(-1);
|
||||
} catch (error) {
|
||||
|
|
602
www/js/util.js
602
www/js/util.js
File diff suppressed because it is too large
Load diff
24310
www/js/video.js
24310
www/js/video.js
File diff suppressed because one or more lines are too long
8
www/js/videojs-contrib-hls.min.js
vendored
8
www/js/videojs-contrib-hls.min.js
vendored
File diff suppressed because one or more lines are too long
|
@ -1,636 +0,0 @@
|
|||
/**
|
||||
* videojs-contrib-dash
|
||||
* @version 2.9.1
|
||||
* @copyright 2017 Brightcove, Inc
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.videojsDash = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
|
||||
(function (global){
|
||||
'use strict';
|
||||
|
||||
exports.__esModule = true;
|
||||
exports['default'] = setupAudioTracks;
|
||||
|
||||
var _dashjs = (typeof window !== "undefined" ? window['dashjs'] : typeof global !== "undefined" ? global['dashjs'] : null);
|
||||
|
||||
var _dashjs2 = _interopRequireDefault(_dashjs);
|
||||
|
||||
var _video = (typeof window !== "undefined" ? window['videojs'] : typeof global !== "undefined" ? global['videojs'] : null);
|
||||
|
||||
var _video2 = _interopRequireDefault(_video);
|
||||
|
||||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
|
||||
|
||||
/**
|
||||
* Setup audio tracks. Take the tracks from dash and add the tracks to videojs. Listen for when
|
||||
* videojs changes tracks and apply that to the dash player because videojs doesn't do this
|
||||
* natively.
|
||||
*
|
||||
* @private
|
||||
* @param {videojs} player the videojs player instance
|
||||
* @param {videojs.tech} tech the videojs tech being used
|
||||
*/
|
||||
function handlePlaybackMetadataLoaded(player, tech) {
|
||||
var mediaPlayer = player.dash.mediaPlayer;
|
||||
|
||||
var dashAudioTracks = mediaPlayer.getTracksFor('audio');
|
||||
var videojsAudioTracks = player.audioTracks();
|
||||
|
||||
function generateIdFromTrackIndex(index) {
|
||||
return 'dash-audio-' + index;
|
||||
}
|
||||
|
||||
function findDashAudioTrack(dashAudioTracks, videojsAudioTrack) {
|
||||
return dashAudioTracks.find(function (_ref) {
|
||||
var index = _ref.index;
|
||||
return generateIdFromTrackIndex(index) === videojsAudioTrack.id;
|
||||
});
|
||||
}
|
||||
|
||||
// Safari creates a single native `AudioTrack` (not `videojs.AudioTrack`) when loading. Clear all
|
||||
// automatically generated audio tracks so we can create them all ourself.
|
||||
if (videojsAudioTracks.length) {
|
||||
tech.clearTracks(['audio']);
|
||||
}
|
||||
|
||||
var currentAudioTrack = mediaPlayer.getCurrentTrackFor('audio');
|
||||
|
||||
dashAudioTracks.forEach(function (dashTrack) {
|
||||
var label = dashTrack.lang;
|
||||
|
||||
if (dashTrack.roles && dashTrack.roles.length) {
|
||||
label += ' (' + dashTrack.roles.join(', ') + ')';
|
||||
}
|
||||
|
||||
// Add the track to the player's audio track list.
|
||||
videojsAudioTracks.addTrack(new _video2['default'].AudioTrack({
|
||||
enabled: dashTrack === currentAudioTrack,
|
||||
id: generateIdFromTrackIndex(dashTrack.index),
|
||||
kind: dashTrack.kind || 'main',
|
||||
label: label,
|
||||
language: dashTrack.lang
|
||||
}));
|
||||
});
|
||||
|
||||
videojsAudioTracks.addEventListener('change', function () {
|
||||
for (var i = 0; i < videojsAudioTracks.length; i++) {
|
||||
var track = videojsAudioTracks[i];
|
||||
|
||||
if (track.enabled) {
|
||||
// Find the audio track we just selected by the id
|
||||
var dashAudioTrack = findDashAudioTrack(dashAudioTracks, track);
|
||||
|
||||
// Set is as the current track
|
||||
mediaPlayer.setCurrentTrack(dashAudioTrack);
|
||||
|
||||
// Stop looping
|
||||
continue;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* Call `handlePlaybackMetadataLoaded` when `mediaPlayer` emits
|
||||
* `dashjs.MediaPlayer.events.PLAYBACK_METADATA_LOADED`.
|
||||
*/
|
||||
function setupAudioTracks(player, tech) {
|
||||
// When `dashjs` finishes loading metadata, create audio tracks for `video.js`.
|
||||
player.dash.mediaPlayer.on(_dashjs2['default'].MediaPlayer.events.PLAYBACK_METADATA_LOADED, handlePlaybackMetadataLoaded.bind(null, player, tech));
|
||||
}
|
||||
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
|
||||
},{}],2:[function(require,module,exports){
|
||||
(function (global){
|
||||
'use strict';
|
||||
|
||||
exports.__esModule = true;
|
||||
exports['default'] = setupTextTracks;
|
||||
|
||||
var _dashjs = (typeof window !== "undefined" ? window['dashjs'] : typeof global !== "undefined" ? global['dashjs'] : null);
|
||||
|
||||
var _dashjs2 = _interopRequireDefault(_dashjs);
|
||||
|
||||
var _video = (typeof window !== "undefined" ? window['videojs'] : typeof global !== "undefined" ? global['videojs'] : null);
|
||||
|
||||
var _video2 = _interopRequireDefault(_video);
|
||||
|
||||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
|
||||
|
||||
function find(l, f) {
|
||||
for (var i = 0; i < l.length; i++) {
|
||||
if (f(l[i])) {
|
||||
return l[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Attach text tracks from dash.js to videojs
|
||||
*
|
||||
* @param {videojs} player the videojs player instance
|
||||
* @param {array} tracks the tracks loaded by dash.js to attach to videojs
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
function attachDashTextTracksToVideojs(player, tech, tracks) {
|
||||
var trackDictionary = [];
|
||||
|
||||
// Add remote tracks
|
||||
var tracksAttached = tracks
|
||||
// Map input data to match HTMLTrackElement spec
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLTrackElement
|
||||
.map(function (track) {
|
||||
return {
|
||||
dashTrack: track,
|
||||
trackConfig: {
|
||||
label: track.lang,
|
||||
language: track.lang,
|
||||
srclang: track.lang
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Add track to videojs track list
|
||||
).map(function (_ref) {
|
||||
var trackConfig = _ref.trackConfig,
|
||||
dashTrack = _ref.dashTrack;
|
||||
|
||||
var remoteTextTrack = player.addRemoteTextTrack(trackConfig, true);
|
||||
trackDictionary.push({ textTrack: remoteTextTrack.track, dashTrack: dashTrack });
|
||||
|
||||
// Don't add the cues becuase we're going to let dash handle it natively. This will ensure
|
||||
// that dash handle external time text files and fragmented text tracks.
|
||||
//
|
||||
// Example file with external time text files:
|
||||
// https://storage.googleapis.com/shaka-demo-assets/sintel-mp4-wvtt/dash.mpd
|
||||
|
||||
return remoteTextTrack;
|
||||
});
|
||||
|
||||
/*
|
||||
* Scan `videojs.textTracks()` to find one that is showing. Set the dash text track.
|
||||
*/
|
||||
function updateActiveDashTextTrack() {
|
||||
var dashMediaPlayer = player.dash.mediaPlayer;
|
||||
var textTracks = player.textTracks();
|
||||
var activeTextTrackIndex = -1;
|
||||
|
||||
// Iterate through the tracks and find the one marked as showing. If none are showing,
|
||||
// `activeTextTrackIndex` will be set to `-1`, disabling text tracks.
|
||||
|
||||
var _loop = function _loop(i) {
|
||||
var textTrack = textTracks[i];
|
||||
|
||||
if (textTrack.mode === 'showing') {
|
||||
// Find the dash track we want to use
|
||||
|
||||
/* jshint loopfunc: true */
|
||||
var dictionaryLookupResult = find(trackDictionary, function (track) {
|
||||
return track.textTrack === textTrack;
|
||||
});
|
||||
/* jshint loopfunc: false */
|
||||
|
||||
var dashTrackToActivate = dictionaryLookupResult ? dictionaryLookupResult.dashTrack : null;
|
||||
|
||||
// If we found a track, get it's index.
|
||||
if (dashTrackToActivate) {
|
||||
activeTextTrackIndex = tracks.indexOf(dashTrackToActivate);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (var i = 0; i < textTracks.length; i += 1) {
|
||||
_loop(i);
|
||||
}
|
||||
|
||||
// If the text track has changed, then set it in dash
|
||||
if (activeTextTrackIndex !== dashMediaPlayer.getCurrentTextTrackIndex()) {
|
||||
dashMediaPlayer.setTextTrack(activeTextTrackIndex);
|
||||
}
|
||||
}
|
||||
|
||||
// Update dash when videojs's selected text track changes.
|
||||
player.textTracks().on('change', updateActiveDashTextTrack);
|
||||
|
||||
// Cleanup event listeners whenever we start loading a new source
|
||||
player.one('loadstart', function () {
|
||||
player.textTracks().off('change', updateActiveDashTextTrack);
|
||||
});
|
||||
|
||||
// Initialize the text track on our first run-through
|
||||
updateActiveDashTextTrack();
|
||||
|
||||
return tracksAttached;
|
||||
}
|
||||
|
||||
/*
|
||||
* Wait for dash to emit `TEXT_TRACKS_ADDED` and then attach the text tracks loaded by dash if
|
||||
* we're not using native text tracks.
|
||||
*
|
||||
* @param {videojs} player the videojs player instance
|
||||
* @private
|
||||
*/
|
||||
function setupTextTracks(player, tech, options) {
|
||||
// Clear VTTCue if it was shimmed by vttjs and let dash.js use TextTrackCue.
|
||||
// This is necessary because dash.js creates text tracks
|
||||
// using addTextTrack which is incompatible with vttjs.VTTCue in IE11
|
||||
if (window.VTTCue && !/\[native code\]/.test(window.VTTCue.toString())) {
|
||||
window.VTTCue = false;
|
||||
}
|
||||
|
||||
// Store the tracks that we've added so we can remove them later.
|
||||
var dashTracksAttachedToVideoJs = [];
|
||||
|
||||
// We're relying on the user to disable native captions. Show an error if they didn't do so.
|
||||
if (tech.featuresNativeTextTracks) {
|
||||
_video2['default'].log.error('You must pass {html: {nativeCaptions: false}} in the videojs constructor ' + 'to use text tracks in videojs-contrib-dash');
|
||||
return;
|
||||
}
|
||||
|
||||
var mediaPlayer = player.dash.mediaPlayer;
|
||||
|
||||
// Clear the tracks that we added. We don't clear them all because someone else can add tracks.
|
||||
function clearDashTracks() {
|
||||
dashTracksAttachedToVideoJs.forEach(player.removeRemoteTextTrack.bind(player));
|
||||
|
||||
dashTracksAttachedToVideoJs = [];
|
||||
}
|
||||
|
||||
function handleTextTracksAdded(_ref2) {
|
||||
var index = _ref2.index,
|
||||
tracks = _ref2.tracks;
|
||||
|
||||
// Stop listening for this event. We only want to hear it once.
|
||||
mediaPlayer.off(_dashjs2['default'].MediaPlayer.events.TEXT_TRACKS_ADDED, handleTextTracksAdded);
|
||||
|
||||
// Cleanup old tracks
|
||||
clearDashTracks();
|
||||
|
||||
if (!tracks.length) {
|
||||
// Don't try to add text tracks if there aren't any
|
||||
return;
|
||||
}
|
||||
|
||||
// Save the tracks so we can remove them later
|
||||
dashTracksAttachedToVideoJs = attachDashTextTracksToVideojs(player, tech, tracks, options);
|
||||
}
|
||||
|
||||
// Attach dash text tracks whenever we dash emits `TEXT_TRACKS_ADDED`.
|
||||
mediaPlayer.on(_dashjs2['default'].MediaPlayer.events.TEXT_TRACKS_ADDED, handleTextTracksAdded);
|
||||
|
||||
function cleanup() {
|
||||
mediaPlayer.off(_dashjs2['default'].MediaPlayer.events.TEXT_TRACKS_ADDED, handleTextTracksAdded);
|
||||
|
||||
player.one('loadstart', clearDashTracks);
|
||||
}
|
||||
|
||||
// When the player can play, remove the initialization events. We might not have received
|
||||
// TEXT_TRACKS_ADDED` so we have to stop listening for it or we'll get errors when we load new
|
||||
// videos and are listening for the same event in multiple places, including cleaned up
|
||||
// mediaPlayers.
|
||||
mediaPlayer.on(_dashjs2['default'].MediaPlayer.events.CAN_PLAY, cleanup);
|
||||
}
|
||||
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
|
||||
},{}],3:[function(require,module,exports){
|
||||
(function (global){
|
||||
var win;
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
win = window;
|
||||
} else if (typeof global !== "undefined") {
|
||||
win = global;
|
||||
} else if (typeof self !== "undefined"){
|
||||
win = self;
|
||||
} else {
|
||||
win = {};
|
||||
}
|
||||
|
||||
module.exports = win;
|
||||
|
||||
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
|
||||
},{}],4:[function(require,module,exports){
|
||||
(function (global){
|
||||
'use strict';
|
||||
|
||||
exports.__esModule = true;
|
||||
|
||||
var _window = require('global/window');
|
||||
|
||||
var _window2 = _interopRequireDefault(_window);
|
||||
|
||||
var _video = (typeof window !== "undefined" ? window['videojs'] : typeof global !== "undefined" ? global['videojs'] : null);
|
||||
|
||||
var _video2 = _interopRequireDefault(_video);
|
||||
|
||||
var _dashjs = (typeof window !== "undefined" ? window['dashjs'] : typeof global !== "undefined" ? global['dashjs'] : null);
|
||||
|
||||
var _dashjs2 = _interopRequireDefault(_dashjs);
|
||||
|
||||
var _setupAudioTracks = require('./setup-audio-tracks');
|
||||
|
||||
var _setupAudioTracks2 = _interopRequireDefault(_setupAudioTracks);
|
||||
|
||||
var _setupTextTracks = require('./setup-text-tracks');
|
||||
|
||||
var _setupTextTracks2 = _interopRequireDefault(_setupTextTracks);
|
||||
|
||||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
|
||||
|
||||
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
|
||||
|
||||
/**
|
||||
* videojs-contrib-dash
|
||||
*
|
||||
* Use Dash.js to playback DASH content inside of Video.js via a SourceHandler
|
||||
*/
|
||||
var Html5DashJS = function () {
|
||||
function Html5DashJS(source, tech, options) {
|
||||
var _this = this;
|
||||
|
||||
_classCallCheck(this, Html5DashJS);
|
||||
|
||||
// Get options from tech if not provided for backwards compatibility
|
||||
options = options || tech.options_;
|
||||
|
||||
this.player = (0, _video2['default'])(options.playerId);
|
||||
this.player.dash = this.player.dash || {};
|
||||
|
||||
this.tech_ = tech;
|
||||
this.el_ = tech.el();
|
||||
this.elParent_ = this.el_.parentNode;
|
||||
|
||||
// Do nothing if the src is falsey
|
||||
if (!source.src) {
|
||||
return;
|
||||
}
|
||||
|
||||
// While the manifest is loading and Dash.js has not finished initializing
|
||||
// we must defer events and functions calls with isReady_ and then `triggerReady`
|
||||
// again later once everything is setup
|
||||
tech.isReady_ = false;
|
||||
|
||||
if (Html5DashJS.updateSourceData) {
|
||||
_video2['default'].log.warn('updateSourceData has been deprecated.' + ' Please switch to using hook("updatesource", callback).');
|
||||
source = Html5DashJS.updateSourceData(source);
|
||||
}
|
||||
|
||||
// call updatesource hooks
|
||||
Html5DashJS.hooks('updatesource').forEach(function (hook) {
|
||||
source = hook(source);
|
||||
});
|
||||
|
||||
var manifestSource = source.src;
|
||||
this.keySystemOptions_ = Html5DashJS.buildDashJSProtData(source.keySystemOptions);
|
||||
|
||||
this.player.dash.mediaPlayer = _dashjs2['default'].MediaPlayer().create();
|
||||
|
||||
this.mediaPlayer_ = this.player.dash.mediaPlayer;
|
||||
|
||||
// Log MedaPlayer messages through video.js
|
||||
if (Html5DashJS.useVideoJSDebug) {
|
||||
_video2['default'].log.warn('useVideoJSDebug has been deprecated.' + ' Please switch to using hook("beforeinitialize", callback).');
|
||||
Html5DashJS.useVideoJSDebug(this.mediaPlayer_);
|
||||
}
|
||||
|
||||
if (Html5DashJS.beforeInitialize) {
|
||||
_video2['default'].log.warn('beforeInitialize has been deprecated.' + ' Please switch to using hook("beforeinitialize", callback).');
|
||||
Html5DashJS.beforeInitialize(this.player, this.mediaPlayer_);
|
||||
}
|
||||
|
||||
Html5DashJS.hooks('beforeinitialize').forEach(function (hook) {
|
||||
hook(_this.player, _this.mediaPlayer_);
|
||||
});
|
||||
|
||||
// Must run controller before these two lines or else there is no
|
||||
// element to bind to.
|
||||
this.mediaPlayer_.initialize();
|
||||
|
||||
// Apply all dash options that are set
|
||||
if (options.dash) {
|
||||
Object.keys(options.dash).forEach(function (key) {
|
||||
var _mediaPlayer_;
|
||||
|
||||
var dashOptionsKey = 'set' + key.charAt(0).toUpperCase() + key.slice(1);
|
||||
var value = options.dash[key];
|
||||
|
||||
if (_this.mediaPlayer_.hasOwnProperty(dashOptionsKey)) {
|
||||
// Providing a key without `set` prefix is now deprecated.
|
||||
_video2['default'].log.warn('Using dash options in videojs-contrib-dash without the set prefix ' + ('has been deprecated. Change \'' + key + '\' to \'' + dashOptionsKey + '\''));
|
||||
|
||||
// Set key so it will still work
|
||||
key = dashOptionsKey;
|
||||
}
|
||||
|
||||
if (!_this.mediaPlayer_.hasOwnProperty(key)) {
|
||||
_video2['default'].log.warn('Warning: dash configuration option unrecognized: ' + key);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Guarantee `value` is an array
|
||||
if (!Array.isArray(value)) {
|
||||
value = [value];
|
||||
}
|
||||
|
||||
(_mediaPlayer_ = _this.mediaPlayer_)[key].apply(_mediaPlayer_, value);
|
||||
});
|
||||
}
|
||||
|
||||
this.mediaPlayer_.attachView(this.el_);
|
||||
|
||||
// Dash.js autoplays by default, video.js will handle autoplay
|
||||
this.mediaPlayer_.setAutoPlay(false);
|
||||
|
||||
// Setup audio tracks
|
||||
_setupAudioTracks2['default'].call(null, this.player, tech);
|
||||
|
||||
// Setup text tracks
|
||||
_setupTextTracks2['default'].call(null, this.player, tech, options);
|
||||
|
||||
// Attach the source with any protection data
|
||||
this.mediaPlayer_.setProtectionData(this.keySystemOptions_);
|
||||
this.mediaPlayer_.attachSource(manifestSource);
|
||||
|
||||
this.tech_.triggerReady();
|
||||
}
|
||||
|
||||
/*
|
||||
* Iterate over the `keySystemOptions` array and convert each object into
|
||||
* the type of object Dash.js expects in the `protData` argument.
|
||||
*
|
||||
* Also rename 'licenseUrl' property in the options to an 'serverURL' property
|
||||
*/
|
||||
|
||||
|
||||
Html5DashJS.buildDashJSProtData = function buildDashJSProtData(keySystemOptions) {
|
||||
var output = {};
|
||||
|
||||
if (!keySystemOptions || !Array.isArray(keySystemOptions)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (var i = 0; i < keySystemOptions.length; i++) {
|
||||
var keySystem = keySystemOptions[i];
|
||||
var options = _video2['default'].mergeOptions({}, keySystem.options);
|
||||
|
||||
if (options.licenseUrl) {
|
||||
options.serverURL = options.licenseUrl;
|
||||
delete options.licenseUrl;
|
||||
}
|
||||
|
||||
output[keySystem.name] = options;
|
||||
}
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
Html5DashJS.prototype.dispose = function dispose() {
|
||||
if (this.mediaPlayer_) {
|
||||
this.mediaPlayer_.reset();
|
||||
}
|
||||
|
||||
if (this.player.dash) {
|
||||
delete this.player.dash;
|
||||
}
|
||||
};
|
||||
|
||||
Html5DashJS.prototype.duration = function duration() {
|
||||
var duration = this.el_.duration;
|
||||
if (duration === Number.MAX_VALUE) {
|
||||
return Infinity;
|
||||
}
|
||||
return duration;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a list of hooks for a specific lifecycle
|
||||
*
|
||||
* @param {string} type the lifecycle to get hooks from
|
||||
* @param {Function=|Function[]=} hook Optionally add a hook tothe lifecycle
|
||||
* @return {Array} an array of hooks or epty if none
|
||||
* @method hooks
|
||||
*/
|
||||
|
||||
|
||||
Html5DashJS.hooks = function hooks(type, hook) {
|
||||
Html5DashJS.hooks_[type] = Html5DashJS.hooks_[type] || [];
|
||||
|
||||
if (hook) {
|
||||
Html5DashJS.hooks_[type] = Html5DashJS.hooks_[type].concat(hook);
|
||||
}
|
||||
|
||||
return Html5DashJS.hooks_[type];
|
||||
};
|
||||
|
||||
/**
|
||||
* Add a function hook to a specific dash lifecycle
|
||||
*
|
||||
* @param {string} type the lifecycle to hook the function to
|
||||
* @param {Function|Function[]} hook the function or array of functions to attach
|
||||
* @method hook
|
||||
*/
|
||||
|
||||
|
||||
Html5DashJS.hook = function hook(type, _hook) {
|
||||
Html5DashJS.hooks(type, _hook);
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove a hook from a specific dash lifecycle.
|
||||
*
|
||||
* @param {string} type the lifecycle that the function hooked to
|
||||
* @param {Function} hook The hooked function to remove
|
||||
* @return {boolean} True if the function was removed, false if not found
|
||||
* @method removeHook
|
||||
*/
|
||||
|
||||
|
||||
Html5DashJS.removeHook = function removeHook(type, hook) {
|
||||
var index = Html5DashJS.hooks(type).indexOf(hook);
|
||||
|
||||
if (index === -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Html5DashJS.hooks_[type] = Html5DashJS.hooks_[type].slice();
|
||||
Html5DashJS.hooks_[type].splice(index, 1);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
return Html5DashJS;
|
||||
}();
|
||||
|
||||
Html5DashJS.hooks_ = {};
|
||||
|
||||
var canHandleKeySystems = function canHandleKeySystems(source) {
|
||||
// copy the source
|
||||
source = JSON.parse(JSON.stringify(source));
|
||||
|
||||
if (Html5DashJS.updateSourceData) {
|
||||
_video2['default'].log.warn('updateSourceData has been deprecated.' + ' Please switch to using hook("updatesource", callback).');
|
||||
source = Html5DashJS.updateSourceData(source);
|
||||
}
|
||||
|
||||
// call updatesource hooks
|
||||
Html5DashJS.hooks('updatesource').forEach(function (hook) {
|
||||
source = hook(source);
|
||||
});
|
||||
|
||||
var videoEl = document.createElement('video');
|
||||
if (source.keySystemOptions && !(navigator.requestMediaKeySystemAccess ||
|
||||
// IE11 Win 8.1
|
||||
videoEl.msSetMediaKeys)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
_video2['default'].DashSourceHandler = function () {
|
||||
return {
|
||||
canHandleSource: function canHandleSource(source) {
|
||||
var dashExtRE = /\.mpd/i;
|
||||
|
||||
if (!canHandleKeySystems(source)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (_video2['default'].DashSourceHandler.canPlayType(source.type)) {
|
||||
return 'probably';
|
||||
} else if (dashExtRE.test(source.src)) {
|
||||
return 'maybe';
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
|
||||
handleSource: function handleSource(source, tech, options) {
|
||||
return new Html5DashJS(source, tech, options);
|
||||
},
|
||||
|
||||
canPlayType: function canPlayType(type) {
|
||||
return _video2['default'].DashSourceHandler.canPlayType(type);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
_video2['default'].DashSourceHandler.canPlayType = function (type) {
|
||||
var dashTypeRE = /^application\/dash\+xml/i;
|
||||
if (dashTypeRE.test(type)) {
|
||||
return 'probably';
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
// Only add the SourceHandler if the browser supports MediaSourceExtensions
|
||||
if (!!_window2['default'].MediaSource) {
|
||||
_video2['default'].getTech('Html5').registerSourceHandler(_video2['default'].DashSourceHandler(), 0);
|
||||
}
|
||||
|
||||
_video2['default'].Html5DashJS = Html5DashJS;
|
||||
exports['default'] = Html5DashJS;
|
||||
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
|
||||
},{"./setup-audio-tracks":1,"./setup-text-tracks":2,"global/window":3}]},{},[4])(4)
|
||||
});
|
19
www/js/vjs/dash.all.min.js
vendored
Normal file
19
www/js/vjs/dash.all.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
www/js/vjs/dash.all.min.js.map
Normal file
1
www/js/vjs/dash.all.min.js.map
Normal file
File diff suppressed because one or more lines are too long
30822
www/js/vjs/video.js
Normal file
30822
www/js/vjs/video.js
Normal file
File diff suppressed because it is too large
Load diff
594
www/js/vjs/videojs-audio-switcher.js
Normal file
594
www/js/vjs/videojs-audio-switcher.js
Normal file
|
@ -0,0 +1,594 @@
|
|||
(function (global, factory) {
|
||||
typeof exports === 'object' && typeof module !== 'undefined' ? factory(require('video.js')) : typeof define === 'function' && define.amd ? define(['video.js'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.videojs));
|
||||
}(this, function (videojs) {
|
||||
'use strict';
|
||||
function _interopDefaultLegacy(e) {
|
||||
return e && typeof e === 'object' && 'default' in e ? e : { 'default': e };
|
||||
}
|
||||
var videojs__default = /*#__PURE__*/
|
||||
_interopDefaultLegacy(videojs);
|
||||
/**
|
||||
* Checks if `value` is the
|
||||
* [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types)
|
||||
* of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`)
|
||||
*
|
||||
* @static
|
||||
* @memberOf _
|
||||
* @since 0.1.0
|
||||
* @category Lang
|
||||
* @param {*} value The value to check.
|
||||
* @returns {boolean} Returns `true` if `value` is an object, else `false`.
|
||||
* @example
|
||||
*
|
||||
* _.isObject({});
|
||||
* // => true
|
||||
*
|
||||
* _.isObject([1, 2, 3]);
|
||||
* // => true
|
||||
*
|
||||
* _.isObject(_.noop);
|
||||
* // => true
|
||||
*
|
||||
* _.isObject(null);
|
||||
* // => false
|
||||
*/
|
||||
function isObject(value) {
|
||||
var type = typeof value;
|
||||
return value != null && (type == 'object' || type == 'function');
|
||||
}
|
||||
/** Detect free variable `global` from Node.js. */
|
||||
var freeGlobal = typeof global == 'object' && global && global.Object === Object && global;
|
||||
/** Detect free variable `self`. */
|
||||
var freeSelf = typeof self == 'object' && self && self.Object === Object && self;
|
||||
/** Used as a reference to the global object. */
|
||||
var root = freeGlobal || freeSelf || Function('return this')();
|
||||
/**
|
||||
* Gets the timestamp of the number of milliseconds that have elapsed since
|
||||
* the Unix epoch (1 January 1970 00:00:00 UTC).
|
||||
*
|
||||
* @static
|
||||
* @memberOf _
|
||||
* @since 2.4.0
|
||||
* @category Date
|
||||
* @returns {number} Returns the timestamp.
|
||||
* @example
|
||||
*
|
||||
* _.defer(function(stamp) {
|
||||
* console.log(_.now() - stamp);
|
||||
* }, _.now());
|
||||
* // => Logs the number of milliseconds it took for the deferred invocation.
|
||||
*/
|
||||
var now = function () {
|
||||
return root.Date.now();
|
||||
};
|
||||
/** Used to match a single whitespace character. */
|
||||
var reWhitespace = /\s/;
|
||||
/**
|
||||
* Used by `_.trim` and `_.trimEnd` to get the index of the last non-whitespace
|
||||
* character of `string`.
|
||||
*
|
||||
* @private
|
||||
* @param {string} string The string to inspect.
|
||||
* @returns {number} Returns the index of the last non-whitespace character.
|
||||
*/
|
||||
function trimmedEndIndex(string) {
|
||||
var index = string.length;
|
||||
while (index-- && reWhitespace.test(string.charAt(index))) {
|
||||
}
|
||||
return index;
|
||||
}
|
||||
/** Used to match leading whitespace. */
|
||||
var reTrimStart = /^\s+/;
|
||||
/**
|
||||
* The base implementation of `_.trim`.
|
||||
*
|
||||
* @private
|
||||
* @param {string} string The string to trim.
|
||||
* @returns {string} Returns the trimmed string.
|
||||
*/
|
||||
function baseTrim(string) {
|
||||
return string ? string.slice(0, trimmedEndIndex(string) + 1).replace(reTrimStart, '') : string;
|
||||
}
|
||||
/** Built-in value references. */
|
||||
var Symbol = root.Symbol;
|
||||
/** Used for built-in method references. */
|
||||
var objectProto$1 = Object.prototype;
|
||||
/** Used to check objects for own properties. */
|
||||
var hasOwnProperty = objectProto$1.hasOwnProperty;
|
||||
/**
|
||||
* Used to resolve the
|
||||
* [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring)
|
||||
* of values.
|
||||
*/
|
||||
var nativeObjectToString$1 = objectProto$1.toString;
|
||||
/** Built-in value references. */
|
||||
var symToStringTag$1 = Symbol ? Symbol.toStringTag : undefined;
|
||||
/**
|
||||
* A specialized version of `baseGetTag` which ignores `Symbol.toStringTag` values.
|
||||
*
|
||||
* @private
|
||||
* @param {*} value The value to query.
|
||||
* @returns {string} Returns the raw `toStringTag`.
|
||||
*/
|
||||
function getRawTag(value) {
|
||||
var isOwn = hasOwnProperty.call(value, symToStringTag$1), tag = value[symToStringTag$1];
|
||||
try {
|
||||
value[symToStringTag$1] = undefined;
|
||||
var unmasked = true;
|
||||
} catch (e) {
|
||||
}
|
||||
var result = nativeObjectToString$1.call(value);
|
||||
if (unmasked) {
|
||||
if (isOwn) {
|
||||
value[symToStringTag$1] = tag;
|
||||
} else {
|
||||
delete value[symToStringTag$1];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
/** Used for built-in method references. */
|
||||
var objectProto = Object.prototype;
|
||||
/**
|
||||
* Used to resolve the
|
||||
* [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring)
|
||||
* of values.
|
||||
*/
|
||||
var nativeObjectToString = objectProto.toString;
|
||||
/**
|
||||
* Converts `value` to a string using `Object.prototype.toString`.
|
||||
*
|
||||
* @private
|
||||
* @param {*} value The value to convert.
|
||||
* @returns {string} Returns the converted string.
|
||||
*/
|
||||
function objectToString(value) {
|
||||
return nativeObjectToString.call(value);
|
||||
}
|
||||
/** `Object#toString` result references. */
|
||||
var nullTag = '[object Null]', undefinedTag = '[object Undefined]';
|
||||
/** Built-in value references. */
|
||||
var symToStringTag = Symbol ? Symbol.toStringTag : undefined;
|
||||
/**
|
||||
* The base implementation of `getTag` without fallbacks for buggy environments.
|
||||
*
|
||||
* @private
|
||||
* @param {*} value The value to query.
|
||||
* @returns {string} Returns the `toStringTag`.
|
||||
*/
|
||||
function baseGetTag(value) {
|
||||
if (value == null) {
|
||||
return value === undefined ? undefinedTag : nullTag;
|
||||
}
|
||||
return symToStringTag && symToStringTag in Object(value) ? getRawTag(value) : objectToString(value);
|
||||
}
|
||||
/**
|
||||
* Checks if `value` is object-like. A value is object-like if it's not `null`
|
||||
* and has a `typeof` result of "object".
|
||||
*
|
||||
* @static
|
||||
* @memberOf _
|
||||
* @since 4.0.0
|
||||
* @category Lang
|
||||
* @param {*} value The value to check.
|
||||
* @returns {boolean} Returns `true` if `value` is object-like, else `false`.
|
||||
* @example
|
||||
*
|
||||
* _.isObjectLike({});
|
||||
* // => true
|
||||
*
|
||||
* _.isObjectLike([1, 2, 3]);
|
||||
* // => true
|
||||
*
|
||||
* _.isObjectLike(_.noop);
|
||||
* // => false
|
||||
*
|
||||
* _.isObjectLike(null);
|
||||
* // => false
|
||||
*/
|
||||
function isObjectLike(value) {
|
||||
return value != null && typeof value == 'object';
|
||||
}
|
||||
/** `Object#toString` result references. */
|
||||
var symbolTag = '[object Symbol]';
|
||||
/**
|
||||
* Checks if `value` is classified as a `Symbol` primitive or object.
|
||||
*
|
||||
* @static
|
||||
* @memberOf _
|
||||
* @since 4.0.0
|
||||
* @category Lang
|
||||
* @param {*} value The value to check.
|
||||
* @returns {boolean} Returns `true` if `value` is a symbol, else `false`.
|
||||
* @example
|
||||
*
|
||||
* _.isSymbol(Symbol.iterator);
|
||||
* // => true
|
||||
*
|
||||
* _.isSymbol('abc');
|
||||
* // => false
|
||||
*/
|
||||
function isSymbol(value) {
|
||||
return typeof value == 'symbol' || isObjectLike(value) && baseGetTag(value) == symbolTag;
|
||||
}
|
||||
/** Used as references for various `Number` constants. */
|
||||
var NAN = 0 / 0;
|
||||
/** Used to detect bad signed hexadecimal string values. */
|
||||
var reIsBadHex = /^[-+]0x[0-9a-f]+$/i;
|
||||
/** Used to detect binary string values. */
|
||||
var reIsBinary = /^0b[01]+$/i;
|
||||
/** Used to detect octal string values. */
|
||||
var reIsOctal = /^0o[0-7]+$/i;
|
||||
/** Built-in method references without a dependency on `root`. */
|
||||
var freeParseInt = parseInt;
|
||||
/**
|
||||
* Converts `value` to a number.
|
||||
*
|
||||
* @static
|
||||
* @memberOf _
|
||||
* @since 4.0.0
|
||||
* @category Lang
|
||||
* @param {*} value The value to process.
|
||||
* @returns {number} Returns the number.
|
||||
* @example
|
||||
*
|
||||
* _.toNumber(3.2);
|
||||
* // => 3.2
|
||||
*
|
||||
* _.toNumber(Number.MIN_VALUE);
|
||||
* // => 5e-324
|
||||
*
|
||||
* _.toNumber(Infinity);
|
||||
* // => Infinity
|
||||
*
|
||||
* _.toNumber('3.2');
|
||||
* // => 3.2
|
||||
*/
|
||||
function toNumber(value) {
|
||||
if (typeof value == 'number') {
|
||||
return value;
|
||||
}
|
||||
if (isSymbol(value)) {
|
||||
return NAN;
|
||||
}
|
||||
if (isObject(value)) {
|
||||
var other = typeof value.valueOf == 'function' ? value.valueOf() : value;
|
||||
value = isObject(other) ? other + '' : other;
|
||||
}
|
||||
if (typeof value != 'string') {
|
||||
return value === 0 ? value : +value;
|
||||
}
|
||||
value = baseTrim(value);
|
||||
var isBinary = reIsBinary.test(value);
|
||||
return isBinary || reIsOctal.test(value) ? freeParseInt(value.slice(2), isBinary ? 2 : 8) : reIsBadHex.test(value) ? NAN : +value;
|
||||
}
|
||||
/** Error message constants. */
|
||||
var FUNC_ERROR_TEXT$1 = 'Expected a function';
|
||||
/* Built-in method references for those with the same name as other `lodash` methods. */
|
||||
var nativeMax = Math.max, nativeMin = Math.min;
|
||||
/**
|
||||
* Creates a debounced function that delays invoking `func` until after `wait`
|
||||
* milliseconds have elapsed since the last time the debounced function was
|
||||
* invoked. The debounced function comes with a `cancel` method to cancel
|
||||
* delayed `func` invocations and a `flush` method to immediately invoke them.
|
||||
* Provide `options` to indicate whether `func` should be invoked on the
|
||||
* leading and/or trailing edge of the `wait` timeout. The `func` is invoked
|
||||
* with the last arguments provided to the debounced function. Subsequent
|
||||
* calls to the debounced function return the result of the last `func`
|
||||
* invocation.
|
||||
*
|
||||
* **Note:** If `leading` and `trailing` options are `true`, `func` is
|
||||
* invoked on the trailing edge of the timeout only if the debounced function
|
||||
* is invoked more than once during the `wait` timeout.
|
||||
*
|
||||
* If `wait` is `0` and `leading` is `false`, `func` invocation is deferred
|
||||
* until to the next tick, similar to `setTimeout` with a timeout of `0`.
|
||||
*
|
||||
* See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/)
|
||||
* for details over the differences between `_.debounce` and `_.throttle`.
|
||||
*
|
||||
* @static
|
||||
* @memberOf _
|
||||
* @since 0.1.0
|
||||
* @category Function
|
||||
* @param {Function} func The function to debounce.
|
||||
* @param {number} [wait=0] The number of milliseconds to delay.
|
||||
* @param {Object} [options={}] The options object.
|
||||
* @param {boolean} [options.leading=false]
|
||||
* Specify invoking on the leading edge of the timeout.
|
||||
* @param {number} [options.maxWait]
|
||||
* The maximum time `func` is allowed to be delayed before it's invoked.
|
||||
* @param {boolean} [options.trailing=true]
|
||||
* Specify invoking on the trailing edge of the timeout.
|
||||
* @returns {Function} Returns the new debounced function.
|
||||
* @example
|
||||
*
|
||||
* // Avoid costly calculations while the window size is in flux.
|
||||
* jQuery(window).on('resize', _.debounce(calculateLayout, 150));
|
||||
*
|
||||
* // Invoke `sendMail` when clicked, debouncing subsequent calls.
|
||||
* jQuery(element).on('click', _.debounce(sendMail, 300, {
|
||||
* 'leading': true,
|
||||
* 'trailing': false
|
||||
* }));
|
||||
*
|
||||
* // Ensure `batchLog` is invoked once after 1 second of debounced calls.
|
||||
* var debounced = _.debounce(batchLog, 250, { 'maxWait': 1000 });
|
||||
* var source = new EventSource('/stream');
|
||||
* jQuery(source).on('message', debounced);
|
||||
*
|
||||
* // Cancel the trailing debounced invocation.
|
||||
* jQuery(window).on('popstate', debounced.cancel);
|
||||
*/
|
||||
function debounce(func, wait, options) {
|
||||
var lastArgs, lastThis, maxWait, result, timerId, lastCallTime, lastInvokeTime = 0, leading = false, maxing = false, trailing = true;
|
||||
if (typeof func != 'function') {
|
||||
throw new TypeError(FUNC_ERROR_TEXT$1);
|
||||
}
|
||||
wait = toNumber(wait) || 0;
|
||||
if (isObject(options)) {
|
||||
leading = !!options.leading;
|
||||
maxing = 'maxWait' in options;
|
||||
maxWait = maxing ? nativeMax(toNumber(options.maxWait) || 0, wait) : maxWait;
|
||||
trailing = 'trailing' in options ? !!options.trailing : trailing;
|
||||
}
|
||||
function invokeFunc(time) {
|
||||
var args = lastArgs, thisArg = lastThis;
|
||||
lastArgs = lastThis = undefined;
|
||||
lastInvokeTime = time;
|
||||
result = func.apply(thisArg, args);
|
||||
return result;
|
||||
}
|
||||
function leadingEdge(time) {
|
||||
// Reset any `maxWait` timer.
|
||||
lastInvokeTime = time;
|
||||
// Start the timer for the trailing edge.
|
||||
timerId = setTimeout(timerExpired, wait);
|
||||
// Invoke the leading edge.
|
||||
return leading ? invokeFunc(time) : result;
|
||||
}
|
||||
function remainingWait(time) {
|
||||
var timeSinceLastCall = time - lastCallTime, timeSinceLastInvoke = time - lastInvokeTime, timeWaiting = wait - timeSinceLastCall;
|
||||
return maxing ? nativeMin(timeWaiting, maxWait - timeSinceLastInvoke) : timeWaiting;
|
||||
}
|
||||
function shouldInvoke(time) {
|
||||
var timeSinceLastCall = time - lastCallTime, timeSinceLastInvoke = time - lastInvokeTime;
|
||||
// Either this is the first call, activity has stopped and we're at the
|
||||
// trailing edge, the system time has gone backwards and we're treating
|
||||
// it as the trailing edge, or we've hit the `maxWait` limit.
|
||||
return lastCallTime === undefined || timeSinceLastCall >= wait || timeSinceLastCall < 0 || maxing && timeSinceLastInvoke >= maxWait;
|
||||
}
|
||||
function timerExpired() {
|
||||
var time = now();
|
||||
if (shouldInvoke(time)) {
|
||||
return trailingEdge(time);
|
||||
}
|
||||
// Restart the timer.
|
||||
timerId = setTimeout(timerExpired, remainingWait(time));
|
||||
}
|
||||
function trailingEdge(time) {
|
||||
timerId = undefined;
|
||||
// Only invoke if we have `lastArgs` which means `func` has been
|
||||
// debounced at least once.
|
||||
if (trailing && lastArgs) {
|
||||
return invokeFunc(time);
|
||||
}
|
||||
lastArgs = lastThis = undefined;
|
||||
return result;
|
||||
}
|
||||
function cancel() {
|
||||
if (timerId !== undefined) {
|
||||
clearTimeout(timerId);
|
||||
}
|
||||
lastInvokeTime = 0;
|
||||
lastArgs = lastCallTime = lastThis = timerId = undefined;
|
||||
}
|
||||
function flush() {
|
||||
return timerId === undefined ? result : trailingEdge(now());
|
||||
}
|
||||
function debounced() {
|
||||
var time = now(), isInvoking = shouldInvoke(time);
|
||||
lastArgs = arguments;
|
||||
lastThis = this;
|
||||
lastCallTime = time;
|
||||
if (isInvoking) {
|
||||
if (timerId === undefined) {
|
||||
return leadingEdge(lastCallTime);
|
||||
}
|
||||
if (maxing) {
|
||||
// Handle invocations in a tight loop.
|
||||
clearTimeout(timerId);
|
||||
timerId = setTimeout(timerExpired, wait);
|
||||
return invokeFunc(lastCallTime);
|
||||
}
|
||||
}
|
||||
if (timerId === undefined) {
|
||||
timerId = setTimeout(timerExpired, wait);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
debounced.cancel = cancel;
|
||||
debounced.flush = flush;
|
||||
return debounced;
|
||||
}
|
||||
/** Error message constants. */
|
||||
var FUNC_ERROR_TEXT = 'Expected a function';
|
||||
/**
|
||||
* Creates a throttled function that only invokes `func` at most once per
|
||||
* every `wait` milliseconds. The throttled function comes with a `cancel`
|
||||
* method to cancel delayed `func` invocations and a `flush` method to
|
||||
* immediately invoke them. Provide `options` to indicate whether `func`
|
||||
* should be invoked on the leading and/or trailing edge of the `wait`
|
||||
* timeout. The `func` is invoked with the last arguments provided to the
|
||||
* throttled function. Subsequent calls to the throttled function return the
|
||||
* result of the last `func` invocation.
|
||||
*
|
||||
* **Note:** If `leading` and `trailing` options are `true`, `func` is
|
||||
* invoked on the trailing edge of the timeout only if the throttled function
|
||||
* is invoked more than once during the `wait` timeout.
|
||||
*
|
||||
* If `wait` is `0` and `leading` is `false`, `func` invocation is deferred
|
||||
* until to the next tick, similar to `setTimeout` with a timeout of `0`.
|
||||
*
|
||||
* See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/)
|
||||
* for details over the differences between `_.throttle` and `_.debounce`.
|
||||
*
|
||||
* @static
|
||||
* @memberOf _
|
||||
* @since 0.1.0
|
||||
* @category Function
|
||||
* @param {Function} func The function to throttle.
|
||||
* @param {number} [wait=0] The number of milliseconds to throttle invocations to.
|
||||
* @param {Object} [options={}] The options object.
|
||||
* @param {boolean} [options.leading=true]
|
||||
* Specify invoking on the leading edge of the timeout.
|
||||
* @param {boolean} [options.trailing=true]
|
||||
* Specify invoking on the trailing edge of the timeout.
|
||||
* @returns {Function} Returns the new throttled function.
|
||||
* @example
|
||||
*
|
||||
* // Avoid excessively updating the position while scrolling.
|
||||
* jQuery(window).on('scroll', _.throttle(updatePosition, 100));
|
||||
*
|
||||
* // Invoke `renewToken` when the click event is fired, but not more than once every 5 minutes.
|
||||
* var throttled = _.throttle(renewToken, 300000, { 'trailing': false });
|
||||
* jQuery(element).on('click', throttled);
|
||||
*
|
||||
* // Cancel the trailing throttled invocation.
|
||||
* jQuery(window).on('popstate', throttled.cancel);
|
||||
*/
|
||||
function throttle(func, wait, options) {
|
||||
var leading = true, trailing = true;
|
||||
if (typeof func != 'function') {
|
||||
throw new TypeError(FUNC_ERROR_TEXT);
|
||||
}
|
||||
if (isObject(options)) {
|
||||
leading = 'leading' in options ? !!options.leading : leading;
|
||||
trailing = 'trailing' in options ? !!options.trailing : trailing;
|
||||
}
|
||||
return debounce(func, wait, {
|
||||
'leading': leading,
|
||||
'maxWait': wait,
|
||||
'trailing': trailing
|
||||
});
|
||||
}
|
||||
const syncTime = (player, audio) => {
|
||||
const time = player.currentTime();
|
||||
audio.currentTime = time;
|
||||
};
|
||||
function audioSwitchPlugin(options) {
|
||||
const {audioElement, audioTracks, debugInterval, syncInterval, volume, handleDisposal} = options;
|
||||
const player = this;
|
||||
const checkAudioElement = () => {
|
||||
const videoElement = player.el_;
|
||||
let newAudio;
|
||||
if (typeof audioElement !== 'undefined' && audioElement instanceof HTMLElement) {
|
||||
newAudio = audioElement;
|
||||
this.audio = newAudio;
|
||||
} else {
|
||||
newAudio = document.createElement('audio');
|
||||
this.audio = newAudio;
|
||||
this.isOurAudio = true;
|
||||
this.audio.className = 'audioSwitch-audio';
|
||||
this.audioParent = document.createElement('div');
|
||||
this.audioParent.className = 'audioSwitch-parent';
|
||||
this.audioParent.appendChild(this.audio);
|
||||
if (videoElement.nextSibling) {
|
||||
videoElement.parentNode.insertBefore(this.audioParent, videoElement.nextSibling);
|
||||
} else {
|
||||
videoElement.parentNode.appendChild(this.audioParent);
|
||||
}
|
||||
}
|
||||
return newAudio;
|
||||
};
|
||||
const audio = checkAudioElement();
|
||||
audio.currentTime = 0;
|
||||
audio.volume = volume;
|
||||
const onAudioTracksChange = (player, audio, event) => {
|
||||
var audioTrackList = player.audioTracks();
|
||||
let enabledTrack;
|
||||
for (let i = 0; i < audioTrackList.length; i++) {
|
||||
let track = audioTrackList[i];
|
||||
if (track.enabled) {
|
||||
enabledTrack = track;
|
||||
break;
|
||||
}
|
||||
}
|
||||
enabledTrack = enabledTrack || audioTrackList[0];
|
||||
if (enabledTrack) {
|
||||
const isPlaying = !player.paused();
|
||||
if (isPlaying)
|
||||
player.pause();
|
||||
audio.setAttribute('src', audioTracks.find(audioTrack => audioTrack.label === enabledTrack.label)?.url);
|
||||
syncTime(player, audio);
|
||||
if (isPlaying)
|
||||
player.play();
|
||||
}
|
||||
};
|
||||
var audioTrackList = player.audioTracks();
|
||||
audioTrackList.addEventListener('change', onAudioTracksChange.bind(null, player, audio));
|
||||
if (audioTracks.length > 0) {
|
||||
audioTracks[0].kind = 'main';
|
||||
audioTracks[0].enabled = true;
|
||||
audioTracks.forEach(track => audioTrackList.addTrack(new videojs.AudioTrack(track)));
|
||||
audio.setAttribute('src', audioTracks[0].url);
|
||||
}
|
||||
player.on('dispose', () => {
|
||||
this.audio.pause();
|
||||
if(this.isOurAudio || handleDisposal){
|
||||
this.audio.remove();
|
||||
this.audioParent.remove();
|
||||
}
|
||||
});
|
||||
player.on('play', () => {
|
||||
syncTime(player, audio);
|
||||
if (audio.paused)
|
||||
audio.play();
|
||||
});
|
||||
player.on('pause', () => {
|
||||
syncTime(player, audio);
|
||||
if (!audio.paused)
|
||||
audio.pause();
|
||||
});
|
||||
player.on('seeked', () => {
|
||||
if (!player.paused())
|
||||
player.pause();
|
||||
player.one('canplay', () => {
|
||||
const sync = () => {
|
||||
syncTime(player, audio);
|
||||
audio.removeEventListener('canplay', sync);
|
||||
if (player.paused())
|
||||
player.play();
|
||||
};
|
||||
audio.addEventListener('canplay', sync);
|
||||
});
|
||||
});
|
||||
player.on('volumechange', () => {
|
||||
if (player.muted()) {
|
||||
audio.muted = true;
|
||||
} else {
|
||||
audio.muted = false;
|
||||
audio.volume = player.volume();
|
||||
}
|
||||
});
|
||||
if (syncInterval) {
|
||||
const syncOnInterval = throttle(() => {
|
||||
syncTime(player, audio);
|
||||
}, syncInterval);
|
||||
player.on('timeupdate', syncOnInterval);
|
||||
}
|
||||
if (debugInterval) {
|
||||
const debugOnInterval = throttle(() => {
|
||||
const _audioBefore = audio.currentTime;
|
||||
const _videoBefore = player.currentTime();
|
||||
console.log('debug', {
|
||||
audio: _audioBefore,
|
||||
video: _videoBefore,
|
||||
diff: _videoBefore - _audioBefore
|
||||
});
|
||||
}, debugInterval);
|
||||
player.on('timeupdate', debugOnInterval);
|
||||
}
|
||||
}
|
||||
videojs__default['default'].registerPlugin('audioSwitch', audioSwitchPlugin);
|
||||
}));
|
69962
www/js/vjs/videojs-dash.js
Normal file
69962
www/js/vjs/videojs-dash.js
Normal file
File diff suppressed because one or more lines are too long
8
www/js/vjs/videojs-hlsjs-plugin.js
Normal file
8
www/js/vjs/videojs-hlsjs-plugin.js
Normal file
File diff suppressed because one or more lines are too long
|
@ -1,6 +1,7 @@
|
|||
/*! videojs-resolution-switcher - 2015-7-26
|
||||
/*! videojs-resolution-switcher - 2022-02-07
|
||||
* Copyright (c) 2016 Kasper Moskwiak
|
||||
* Modified by Pierre Kraft and Derk-Jan Hartman
|
||||
* Modified by Nisaba
|
||||
* Licensed under the Apache-2.0 license. */
|
||||
|
||||
(function() {
|
||||
|
@ -58,11 +59,11 @@
|
|||
this.controlText('Quality');
|
||||
|
||||
if(options.dynamicLabel){
|
||||
videojs.addClass(this.label, 'vjs-resolution-button-label');
|
||||
videojs.dom.addClass(this.label, 'vjs-resolution-button-label');
|
||||
this.el().appendChild(this.label);
|
||||
}else{
|
||||
var staticLabel = document.createElement('span');
|
||||
videojs.addClass(staticLabel, 'vjs-menu-icon');
|
||||
videojs.dom.addClass(staticLabel, 'vjs-menu-icon');
|
||||
this.el().appendChild(staticLabel);
|
||||
}
|
||||
player.on('updateSources', videojs.bind( this, this.update ) );
|
||||
|
@ -167,9 +168,7 @@
|
|||
}
|
||||
|
||||
// Change player source and wait for loadeddata event, then play video
|
||||
// loadedmetadata doesn't work right now for flash.
|
||||
// Probably because of https://github.com/videojs/video-js-swf/issues/124
|
||||
// If player preload is 'none' and then loadeddata not fired. So, we need timeupdate event for seek handle (timeupdate doesn't work properly with flash)
|
||||
// If player preload is 'none' and then loadeddata not fired. So, we need timeupdate event for seek handle
|
||||
var handleSeekEvent = 'loadeddata';
|
||||
if(this.player_.techName_ !== 'Youtube' && this.player_.preload() === 'none' && this.player_.techName_ !== 'Flash') {
|
||||
handleSeekEvent = 'timeupdate';
|
||||
|
@ -178,10 +177,8 @@
|
|||
.setSourcesSanitized(sources, label, customSourcePicker || settings.customSourcePicker)
|
||||
.one(handleSeekEvent, function() {
|
||||
player.currentTime(currentTime);
|
||||
player.handleTechSeeked_();
|
||||
if(!isPaused){
|
||||
// Start playing and hide loadingSpinner (flash issue ?)
|
||||
player.play().handleTechSeeked_();
|
||||
if (!isPaused && player.paused()) {
|
||||
player.play()
|
||||
}
|
||||
player.trigger('resolutionchange');
|
||||
});
|
||||
|
@ -348,7 +345,7 @@
|
|||
};
|
||||
}
|
||||
if(player.options_.sources.length > 1){
|
||||
// tech: Html5 and Flash
|
||||
// tech: Html5
|
||||
// Create resolution switcher for videos form <source> tag inside <video>
|
||||
player.updateSrc(player.options_.sources);
|
||||
}
|
||||
|
@ -362,6 +359,6 @@
|
|||
};
|
||||
|
||||
// register the plugin
|
||||
videojs.plugin('videoJsResolutionSwitcher', videoJsResolutionSwitcher);
|
||||
videojs.registerPlugin('videoJsResolutionSwitcher', videoJsResolutionSwitcher);
|
||||
})(window, videojs);
|
||||
})();
|
BIN
www/video-js.swf
BIN
www/video-js.swf
Binary file not shown.
Loading…
Reference in a new issue