mirror of
https://github.com/Rafostar/clapper.git
synced 2025-08-29 15:22:11 +02:00
Compare commits
2 Commits
cdfac76d66
...
gtuber
Author | SHA1 | Date | |
---|---|---|---|
|
2076309aaa | ||
|
79618edd1e |
@@ -103,14 +103,14 @@
|
||||
<summary>Set PlayFlags for playbin</summary>
|
||||
</key>
|
||||
|
||||
<!-- YouTube -->
|
||||
<!-- Gtuber -->
|
||||
<key name="yt-adaptive-enabled" type="b">
|
||||
<default>false</default>
|
||||
<summary>Enable to use adaptive streaming for YouTube</summary>
|
||||
<summary>Enable to use adaptive streaming</summary>
|
||||
</key>
|
||||
<key name="yt-quality-type" type="i">
|
||||
<default>1</default>
|
||||
<summary>Max YouTube video quality type</summary>
|
||||
<summary>Max online video quality type</summary>
|
||||
</key>
|
||||
|
||||
<!-- Other -->
|
||||
|
@@ -33,6 +33,7 @@
|
||||
"flathub/lib/libass.json",
|
||||
"flathub/lib/ffmpeg.json",
|
||||
"testing/gstreamer.json",
|
||||
"testing/gtuber.json",
|
||||
{
|
||||
"name": "clapper",
|
||||
"buildsystem": "meson",
|
||||
|
@@ -42,6 +42,7 @@
|
||||
"flathub/gstreamer-1.0/gstreamer-vaapi.json",
|
||||
"flathub/lib/gtk4.json",
|
||||
"flathub/lib/libadwaita.json",
|
||||
"testing/gtuber.json",
|
||||
{
|
||||
"name": "clapper",
|
||||
"buildsystem": "meson",
|
||||
|
18
pkgs/flatpak/testing/gtuber.json
Normal file
18
pkgs/flatpak/testing/gtuber.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "gtuber",
|
||||
"buildsystem": "meson",
|
||||
"config-opts": [
|
||||
"-Dvapi=disabled"
|
||||
],
|
||||
"cleanup": [
|
||||
"/include",
|
||||
"/lib/pkgconfig"
|
||||
],
|
||||
"sources": [
|
||||
{
|
||||
"type": "git",
|
||||
"url": "https://github.com/Rafostar/gtuber.git",
|
||||
"branch": "main"
|
||||
}
|
||||
]
|
||||
}
|
@@ -1,151 +0,0 @@
|
||||
/* Copyright (C) 2012-present by fent
|
||||
*
|
||||
* 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:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
const jsVarStr = '[a-zA-Z_\\$][a-zA-Z_0-9]*';
|
||||
const jsSingleQuoteStr = `'[^'\\\\]*(:?\\\\[\\s\\S][^'\\\\]*)*'`;
|
||||
const jsDoubleQuoteStr = `"[^"\\\\]*(:?\\\\[\\s\\S][^"\\\\]*)*"`;
|
||||
const jsQuoteStr = `(?:${jsSingleQuoteStr}|${jsDoubleQuoteStr})`;
|
||||
const jsKeyStr = `(?:${jsVarStr}|${jsQuoteStr})`;
|
||||
const jsPropStr = `(?:\\.${jsVarStr}|\\[${jsQuoteStr}\\])`;
|
||||
const jsEmptyStr = `(?:''|"")`;
|
||||
const reverseStr = ':function\\(a\\)\\{' +
|
||||
'(?:return )?a\\.reverse\\(\\)' +
|
||||
'\\}';
|
||||
const sliceStr = ':function\\(a,b\\)\\{' +
|
||||
'return a\\.slice\\(b\\)' +
|
||||
'\\}';
|
||||
const spliceStr = ':function\\(a,b\\)\\{' +
|
||||
'a\\.splice\\(0,b\\)' +
|
||||
'\\}';
|
||||
const swapStr = ':function\\(a,b\\)\\{' +
|
||||
'var c=a\\[0\\];a\\[0\\]=a\\[b(?:%a\\.length)?\\];a\\[b(?:%a\\.length)?\\]=c(?:;return a)?' +
|
||||
'\\}';
|
||||
const actionsObjRegexp = new RegExp(
|
||||
`var (${jsVarStr})=\\{((?:(?:${
|
||||
jsKeyStr}${reverseStr}|${
|
||||
jsKeyStr}${sliceStr}|${
|
||||
jsKeyStr}${spliceStr}|${
|
||||
jsKeyStr}${swapStr
|
||||
}),?\\r?\\n?)+)\\};`);
|
||||
const actionsFuncRegexp = new RegExp(`${`function(?: ${jsVarStr})?\\(a\\)\\{` +
|
||||
`a=a\\.split\\(${jsEmptyStr}\\);\\s*` +
|
||||
`((?:(?:a=)?${jsVarStr}`}${
|
||||
jsPropStr
|
||||
}\\(a,\\d+\\);)+)` +
|
||||
`return a\\.join\\(${jsEmptyStr}\\)` +
|
||||
`\\}`);
|
||||
const reverseRegexp = new RegExp(`(?:^|,)(${jsKeyStr})${reverseStr}`, 'm');
|
||||
const sliceRegexp = new RegExp(`(?:^|,)(${jsKeyStr})${sliceStr}`, 'm');
|
||||
const spliceRegexp = new RegExp(`(?:^|,)(${jsKeyStr})${spliceStr}`, 'm');
|
||||
const swapRegexp = new RegExp(`(?:^|,)(${jsKeyStr})${swapStr}`, 'm');
|
||||
|
||||
const swapHeadAndPosition = (arr, position) => {
|
||||
const first = arr[0];
|
||||
arr[0] = arr[position % arr.length];
|
||||
arr[position] = first;
|
||||
|
||||
return arr;
|
||||
}
|
||||
|
||||
function decipher(sig, tokens) {
|
||||
sig = sig.split('');
|
||||
tokens = tokens.split(',');
|
||||
|
||||
for(let i = 0, len = tokens.length; i < len; i++) {
|
||||
let token = tokens[i], pos;
|
||||
switch (token[0]) {
|
||||
case 'r':
|
||||
sig = sig.reverse();
|
||||
break;
|
||||
case 'w':
|
||||
pos = ~~token.slice(1);
|
||||
sig = swapHeadAndPosition(sig, pos);
|
||||
break;
|
||||
case 's':
|
||||
pos = ~~token.slice(1);
|
||||
sig = sig.slice(pos);
|
||||
break;
|
||||
case 'p':
|
||||
pos = ~~token.slice(1);
|
||||
sig.splice(0, pos);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return sig.join('');
|
||||
};
|
||||
|
||||
function extractActions(body) {
|
||||
const objResult = actionsObjRegexp.exec(body);
|
||||
const funcResult = actionsFuncRegexp.exec(body);
|
||||
|
||||
if(!objResult || !funcResult)
|
||||
return null;
|
||||
|
||||
const obj = objResult[1].replace(/\$/g, '\\$');
|
||||
const objBody = objResult[2].replace(/\$/g, '\\$');
|
||||
const funcBody = funcResult[1].replace(/\$/g, '\\$');
|
||||
|
||||
let result = reverseRegexp.exec(objBody);
|
||||
const reverseKey = result && result[1]
|
||||
.replace(/\$/g, '\\$')
|
||||
.replace(/\$|^'|^"|'$|"$/g, '');
|
||||
result = sliceRegexp.exec(objBody);
|
||||
const sliceKey = result && result[1]
|
||||
.replace(/\$/g, '\\$')
|
||||
.replace(/\$|^'|^"|'$|"$/g, '');
|
||||
result = spliceRegexp.exec(objBody);
|
||||
const spliceKey = result && result[1]
|
||||
.replace(/\$/g, '\\$')
|
||||
.replace(/\$|^'|^"|'$|"$/g, '');
|
||||
result = swapRegexp.exec(objBody);
|
||||
const swapKey = result && result[1]
|
||||
.replace(/\$/g, '\\$')
|
||||
.replace(/\$|^'|^"|'$|"$/g, '');
|
||||
|
||||
const keys = `(${[reverseKey, sliceKey, spliceKey, swapKey].join('|')})`;
|
||||
const myreg = `(?:a=)?${obj
|
||||
}(?:\\.${keys}|\\['${keys}'\\]|\\["${keys}"\\])` +
|
||||
`\\(a,(\\d+)\\)`;
|
||||
const tokenizeRegexp = new RegExp(myreg, 'g');
|
||||
const tokens = [];
|
||||
|
||||
while((result = tokenizeRegexp.exec(funcBody)) !== null) {
|
||||
const key = result[1] || result[2] || result[3];
|
||||
const pos = result[4];
|
||||
switch (key) {
|
||||
case swapKey:
|
||||
tokens.push(`w${result[4]}`);
|
||||
break;
|
||||
case reverseKey:
|
||||
tokens.push('r');
|
||||
break;
|
||||
case sliceKey:
|
||||
tokens.push(`s${result[4]}`);
|
||||
break;
|
||||
case spliceKey:
|
||||
tokens.push(`p${result[4]}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return tokens.join(',');
|
||||
}
|
38
src/debug.js
38
src/debug.js
@@ -14,22 +14,13 @@ const clapperDebugger = new Debug.Debugger('Clapper', {
|
||||
}),
|
||||
high_precision: true,
|
||||
});
|
||||
clapperDebugger.enabled = (
|
||||
|
||||
var enabled = (
|
||||
clapperDebugger.enabled
|
||||
|| G_DEBUG_ENV != null
|
||||
&& G_DEBUG_ENV.includes('Clapper')
|
||||
);
|
||||
|
||||
const ytDebugger = new Debug.Debugger('YouTube', {
|
||||
name_printer: new Ink.Printer({
|
||||
font: Ink.Font.BOLD,
|
||||
color: Ink.Color.RED
|
||||
}),
|
||||
time_printer: new Ink.Printer({
|
||||
color: Ink.Color.LIGHT_BLUE
|
||||
}),
|
||||
high_precision: true,
|
||||
});
|
||||
clapperDebugger.enabled = enabled;
|
||||
|
||||
function _logStructured(debuggerName, msg, level)
|
||||
{
|
||||
@@ -43,23 +34,12 @@ function _logStructured(debuggerName, msg, level)
|
||||
function _debug(debuggerName, msg)
|
||||
{
|
||||
if(msg.message) {
|
||||
_logStructured(
|
||||
debuggerName,
|
||||
msg.message,
|
||||
_logStructured(debuggerName, msg.message,
|
||||
GLib.LogLevelFlags.LEVEL_CRITICAL
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
switch(debuggerName) {
|
||||
case 'Clapper':
|
||||
clapperDebugger.debug(msg);
|
||||
break;
|
||||
case 'YouTube':
|
||||
ytDebugger.debug(msg);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function debug(msg)
|
||||
@@ -67,12 +47,12 @@ function debug(msg)
|
||||
_debug('Clapper', msg);
|
||||
}
|
||||
|
||||
function ytDebug(msg)
|
||||
{
|
||||
_debug('YouTube', msg);
|
||||
}
|
||||
|
||||
function warn(msg)
|
||||
{
|
||||
_logStructured('Clapper', msg, GLib.LogLevelFlags.LEVEL_WARNING);
|
||||
}
|
||||
|
||||
function message(msg)
|
||||
{
|
||||
_logStructured('Clapper', msg, GLib.LogLevelFlags.LEVEL_MESSAGE);
|
||||
}
|
||||
|
297
src/gtuber.js
Normal file
297
src/gtuber.js
Normal file
@@ -0,0 +1,297 @@
|
||||
const { Gio, GstClapper } = imports.gi;
|
||||
const Debug = imports.src.debug;
|
||||
const Misc = imports.src.misc;
|
||||
const FileOps = imports.src.fileOps;
|
||||
const Gtuber = Misc.tryImport('Gtuber');
|
||||
|
||||
const { debug, warn } = Debug;
|
||||
const { settings } = Misc;
|
||||
|
||||
const best = {
|
||||
video: null,
|
||||
audio: null,
|
||||
video_audio: null,
|
||||
};
|
||||
const codecPairs = [];
|
||||
const qualityType = {
|
||||
0: 30, // normal
|
||||
1: 60, // hfr
|
||||
};
|
||||
|
||||
var isAvailable = (Gtuber != null);
|
||||
var cancellable = null;
|
||||
let client = null;
|
||||
|
||||
function resetBestStreams()
|
||||
{
|
||||
best.video = null;
|
||||
best.audio = null;
|
||||
best.video_audio = null;
|
||||
}
|
||||
|
||||
function isStreamAllowed(stream, opts)
|
||||
{
|
||||
const vcodec = stream.video_codec;
|
||||
const acodec = stream.audio_codec;
|
||||
|
||||
if(
|
||||
vcodec
|
||||
&& (!vcodec.startsWith(opts.vcodec)
|
||||
|| (stream.height < 240 || stream.height > opts.height)
|
||||
|| stream.fps > qualityType[opts.quality])
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(
|
||||
acodec
|
||||
&& (!acodec.startsWith(opts.acodec))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (vcodec != null || acodec != null);
|
||||
}
|
||||
|
||||
function updateBestStreams(streams, opts)
|
||||
{
|
||||
for(let stream of streams) {
|
||||
if(!isStreamAllowed(stream, opts))
|
||||
continue;
|
||||
|
||||
const type = (stream.video_codec && stream.audio_codec)
|
||||
? 'video_audio'
|
||||
: (stream.video_codec)
|
||||
? 'video'
|
||||
: 'audio';
|
||||
|
||||
if(!best[type] || best[type].bitrate < stream.bitrate)
|
||||
best[type] = stream;
|
||||
}
|
||||
}
|
||||
|
||||
function _streamFilter(opts, stream)
|
||||
{
|
||||
switch(stream) {
|
||||
case best.video:
|
||||
return (best.audio != null || best.video_audio == null);
|
||||
case best.audio:
|
||||
return (best.video != null || best.video_audio == null);
|
||||
case best.video_audio:
|
||||
return (best.video == null || best.audio == null);
|
||||
default:
|
||||
return (opts.adaptive)
|
||||
? isStreamAllowed(stream, opts)
|
||||
: false;
|
||||
}
|
||||
}
|
||||
|
||||
function generateManifest(info, opts)
|
||||
{
|
||||
const gen = new Gtuber.ManifestGenerator({
|
||||
pretty: Debug.enabled,
|
||||
});
|
||||
gen.set_media_info(info);
|
||||
gen.set_filter_func(_streamFilter.bind(this, opts));
|
||||
|
||||
debug('trying to get manifest');
|
||||
|
||||
for(let pair of codecPairs) {
|
||||
opts.vcodec = pair[0];
|
||||
opts.acodec = pair[1];
|
||||
|
||||
/* Find best streams among adaptive ones */
|
||||
if (!opts.adaptive)
|
||||
updateBestStreams(info.get_adaptive_streams(), opts);
|
||||
|
||||
const data = gen.to_data();
|
||||
|
||||
/* Release our ref */
|
||||
if (!opts.adaptive)
|
||||
resetBestStreams();
|
||||
|
||||
if(data) {
|
||||
debug('got manifest');
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
debug('manifest not generated');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function getBestCombinedUri(info, opts)
|
||||
{
|
||||
const streams = info.get_streams();
|
||||
|
||||
debug('searching for best combined URI');
|
||||
|
||||
for(let pair of codecPairs) {
|
||||
opts.vcodec = pair[0];
|
||||
opts.acodec = pair[1];
|
||||
|
||||
/* Find best non-adaptive stream */
|
||||
updateBestStreams(streams, opts);
|
||||
|
||||
const bestUri = (best.video_audio)
|
||||
? best.video_audio.get_uri()
|
||||
: (best.audio)
|
||||
? best.audio.get_uri()
|
||||
: (best.video)
|
||||
? best.video.get_uri()
|
||||
: null;
|
||||
|
||||
/* Release our ref */
|
||||
resetBestStreams();
|
||||
|
||||
if(bestUri) {
|
||||
debug('got best possible URI');
|
||||
return bestUri;
|
||||
}
|
||||
}
|
||||
|
||||
/* If still nothing find stream by height */
|
||||
for(let stream of streams) {
|
||||
const height = stream.get_height();
|
||||
if(!height || height > opts.height)
|
||||
continue;
|
||||
|
||||
if(!best.video_audio || best.video_audio.height < stream.height)
|
||||
best.video_audio = stream;
|
||||
}
|
||||
|
||||
const anyUri = (best.video_audio)
|
||||
? best.video_audio.get_uri()
|
||||
: null;
|
||||
|
||||
/* Release our ref */
|
||||
resetBestStreams();
|
||||
|
||||
if (anyUri)
|
||||
debug('got any URI');
|
||||
|
||||
return anyUri;
|
||||
}
|
||||
|
||||
async function _parseMediaInfoAsync(info, player)
|
||||
{
|
||||
const resp = {
|
||||
uri: null,
|
||||
title: info.title,
|
||||
};
|
||||
|
||||
const { root } = player.widget;
|
||||
const surface = root.get_surface();
|
||||
const monitor = root.display.get_monitor_at_surface(surface);
|
||||
|
||||
const opts = {
|
||||
width: monitor.geometry.width * monitor.scale_factor,
|
||||
height: monitor.geometry.height * monitor.scale_factor,
|
||||
vcodec: null,
|
||||
acodec: null,
|
||||
quality: settings.get_int('yt-quality-type'),
|
||||
adaptive: settings.get_boolean('yt-adaptive-enabled'),
|
||||
};
|
||||
|
||||
if(info.has_adaptive_streams) {
|
||||
const data = generateManifest(info, opts);
|
||||
if(data) {
|
||||
const manifestFile = await FileOps.saveFilePromise(
|
||||
'tmp', null, 'manifest', data
|
||||
).catch(debug);
|
||||
|
||||
if(!manifestFile)
|
||||
throw new Error('Gtuber: no manifest file was generated');
|
||||
|
||||
resp.uri = manifestFile.get_uri();
|
||||
|
||||
return resp;
|
||||
}
|
||||
}
|
||||
|
||||
resp.uri = getBestCombinedUri(info, opts);
|
||||
|
||||
if(!resp.uri)
|
||||
throw new Error("Gtuber: no compatible stream found");
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
||||
function _createClient(player)
|
||||
{
|
||||
client = new Gtuber.Client();
|
||||
debug('created new gtuber client');
|
||||
|
||||
/* TODO: config based on what HW supports */
|
||||
//codecPairs.push(['vp9', 'opus']);
|
||||
|
||||
codecPairs.push(['avc', 'mp4a']);
|
||||
}
|
||||
|
||||
function mightHandleUri(uri)
|
||||
{
|
||||
const unsupported = [
|
||||
'file', 'fd', 'dvd', 'cdda',
|
||||
'dvb', 'v4l2', 'gs'
|
||||
];
|
||||
return !unsupported.includes(Misc.getUriProtocol(uri));
|
||||
}
|
||||
|
||||
function cancelFetching()
|
||||
{
|
||||
if(cancellable && !cancellable.is_cancelled())
|
||||
cancellable.cancel();
|
||||
}
|
||||
|
||||
function parseUriPromise(uri, player)
|
||||
{
|
||||
return new Promise((resolve, reject) => {
|
||||
if(!client) {
|
||||
if(!isAvailable) {
|
||||
debug('gtuber is not installed');
|
||||
return resolve({ uri, title: null });
|
||||
}
|
||||
_createClient(player);
|
||||
}
|
||||
|
||||
/* Stop to show reaction and restore internet bandwidth */
|
||||
if(player.state !== GstClapper.ClapperState.STOPPED)
|
||||
player.stop();
|
||||
|
||||
cancellable = new Gio.Cancellable();
|
||||
debug('gtuber is fetching media info...');
|
||||
|
||||
client.fetch_media_info_async(uri, cancellable, (client, task) => {
|
||||
cancellable = null;
|
||||
let info = null;
|
||||
|
||||
try {
|
||||
info = client.fetch_media_info_finish(task);
|
||||
debug('gtuber successfully fetched media info');
|
||||
}
|
||||
catch(err) {
|
||||
const taskCancellable = task.get_cancellable();
|
||||
|
||||
if(taskCancellable.is_cancelled())
|
||||
return reject(err);
|
||||
|
||||
const gtuberNoPlugin = (
|
||||
err.domain === Gtuber.ClientError.quark()
|
||||
&& err.code === Gtuber.ClientError.NO_PLUGIN
|
||||
);
|
||||
if(!gtuberNoPlugin)
|
||||
return reject(err);
|
||||
|
||||
warn(`Gtuber: ${err.message}, trying URI as is...`);
|
||||
|
||||
/* Allow handling URI as is via GStreamer plugins */
|
||||
return resolve({ uri, title: null });
|
||||
}
|
||||
|
||||
_parseMediaInfoAsync(info, player)
|
||||
.then(resp => resolve(resp))
|
||||
.catch(err => reject(err));
|
||||
});
|
||||
});
|
||||
}
|
@@ -1,6 +1,7 @@
|
||||
imports.gi.versions.Gdk = '4.0';
|
||||
imports.gi.versions.Gtk = '4.0';
|
||||
imports.gi.versions.Soup = '2.4';
|
||||
imports.gi.versions.Gtuber = '0.0';
|
||||
|
||||
pkg.initGettext();
|
||||
pkg.initFormat();
|
||||
|
@@ -1,6 +1,7 @@
|
||||
imports.gi.versions.Gdk = '4.0';
|
||||
imports.gi.versions.Gtk = '4.0';
|
||||
imports.gi.versions.Soup = '2.4';
|
||||
imports.gi.versions.Gtuber = '0.0';
|
||||
|
||||
pkg.initGettext();
|
||||
|
||||
|
39
src/misc.js
39
src/misc.js
@@ -1,7 +1,8 @@
|
||||
const { Gio, GLib, Gdk, Gtk } = imports.gi;
|
||||
const Debug = imports.src.debug;
|
||||
|
||||
const { debug } = Debug;
|
||||
const { debug, message } = Debug;
|
||||
const failedImports = [];
|
||||
|
||||
var appName = 'Clapper';
|
||||
var appId = 'com.github.rafostar.Clapper';
|
||||
@@ -28,6 +29,23 @@ const subsKeys = Object.keys(subsTitles);
|
||||
|
||||
let inhibitCookie;
|
||||
|
||||
function tryImport(libName)
|
||||
{
|
||||
let lib = null;
|
||||
|
||||
try {
|
||||
lib = imports.gi[libName];
|
||||
}
|
||||
catch(err) {
|
||||
if(!failedImports.includes(libName)) {
|
||||
failedImports.push(libName);
|
||||
message(err.message);
|
||||
}
|
||||
}
|
||||
|
||||
return lib;
|
||||
}
|
||||
|
||||
function getResourceUri(path)
|
||||
{
|
||||
const res = `file://${pkg.pkgdatadir}/${path}`;
|
||||
@@ -224,22 +242,3 @@ function getIsTouch(gesture)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function encodeHTML(text)
|
||||
{
|
||||
return text.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function decodeURIPlus(uri)
|
||||
{
|
||||
return decodeURI(uri.replace(/\+/g, ' '));
|
||||
}
|
||||
|
||||
function isHex(num)
|
||||
{
|
||||
return Boolean(num.match(/[0-9a-f]+$/i));
|
||||
}
|
||||
|
@@ -2,7 +2,7 @@ const { Gdk, Gio, GObject, Gst, GstClapper, Gtk } = imports.gi;
|
||||
const ByteArray = imports.byteArray;
|
||||
const Debug = imports.src.debug;
|
||||
const Misc = imports.src.misc;
|
||||
const YouTube = imports.src.youtube;
|
||||
const Gtuber = imports.src.gtuber;
|
||||
const { PlaylistWidget } = imports.src.playlist;
|
||||
const { WebApp } = imports.src.webApp;
|
||||
|
||||
@@ -45,7 +45,6 @@ class ClapperPlayer extends GstClapper.Clapper
|
||||
|
||||
this.webserver = null;
|
||||
this.webapp = null;
|
||||
this.ytClient = null;
|
||||
this.playlistWidget = new PlaylistWidget();
|
||||
|
||||
this.seekDone = true;
|
||||
@@ -142,24 +141,13 @@ class ClapperPlayer extends GstClapper.Clapper
|
||||
set_uri(uri)
|
||||
{
|
||||
this.customVideoTitle = null;
|
||||
Gtuber.cancelFetching();
|
||||
|
||||
if(Misc.getUriProtocol(uri) !== 'file') {
|
||||
const [isYouTubeUri, videoId] = YouTube.checkYouTubeUri(uri);
|
||||
|
||||
if(!isYouTubeUri)
|
||||
return super.set_uri(uri);
|
||||
|
||||
if(!this.ytClient)
|
||||
this.ytClient = new YouTube.YouTubeClient();
|
||||
|
||||
const { root } = this.widget;
|
||||
const surface = root.get_surface();
|
||||
const monitor = root.display.get_monitor_at_surface(surface);
|
||||
|
||||
this.ytClient.getPlaybackDataAsync(videoId, monitor)
|
||||
.then(data => {
|
||||
this.customVideoTitle = data.title;
|
||||
super.set_uri(data.uri);
|
||||
if(Gtuber.mightHandleUri(uri)) {
|
||||
Gtuber.parseUriPromise(uri, this)
|
||||
.then(res => {
|
||||
this.customVideoTitle = res.title;
|
||||
super.set_uri(res.uri);
|
||||
})
|
||||
.catch(debug);
|
||||
|
||||
|
@@ -1,6 +1,7 @@
|
||||
const { Adw, GObject, Gio, Gst, Gtk } = imports.gi;
|
||||
const Debug = imports.src.debug;
|
||||
const Misc = imports.src.misc;
|
||||
const Gtuber = imports.src.gtuber;
|
||||
|
||||
const { debug } = Debug;
|
||||
const { settings } = Misc;
|
||||
@@ -537,6 +538,7 @@ class ClapperPrefsPluginRankingSubpage extends Gtk.Box
|
||||
var PrefsWindow = GObject.registerClass({
|
||||
GTypeName: 'ClapperPrefsWindow',
|
||||
Template: Misc.getResourceUri('ui/preferences-window.ui'),
|
||||
InternalChildren: ['gtuber_group'],
|
||||
},
|
||||
class ClapperPrefsWindow extends Adw.PreferencesWindow
|
||||
{
|
||||
@@ -546,6 +548,7 @@ class ClapperPrefsWindow extends Adw.PreferencesWindow
|
||||
transient_for: window,
|
||||
});
|
||||
|
||||
this._gtuber_group.visible = Gtuber.isAvailable;
|
||||
this.show();
|
||||
}
|
||||
});
|
||||
|
@@ -4,7 +4,6 @@ const Debug = imports.src.debug;
|
||||
const Dialogs = imports.src.dialogs;
|
||||
const Misc = imports.src.misc;
|
||||
const { Player } = imports.src.player;
|
||||
const YouTube = imports.src.youtube;
|
||||
const Revealers = imports.src.revealers;
|
||||
|
||||
const { debug } = Debug;
|
||||
@@ -803,12 +802,10 @@ class ClapperWidget extends Gtk.Grid
|
||||
{
|
||||
const dropTarget = new Gtk.DropTarget({
|
||||
actions: Gdk.DragAction.COPY | Gdk.DragAction.MOVE,
|
||||
preload: true,
|
||||
});
|
||||
dropTarget.set_gtypes([GObject.TYPE_STRING]);
|
||||
dropTarget.connect('motion', this._onDataMotion.bind(this));
|
||||
dropTarget.connect('drop', this._onDataDrop.bind(this));
|
||||
dropTarget.connect('notify::value', this._onDropValueNotify.bind(this));
|
||||
|
||||
return dropTarget;
|
||||
}
|
||||
@@ -1009,36 +1006,6 @@ class ClapperWidget extends Gtk.Grid
|
||||
this.posY = posY;
|
||||
}
|
||||
|
||||
_onDropValueNotify(dropTarget)
|
||||
{
|
||||
if(!dropTarget.value)
|
||||
return;
|
||||
|
||||
const uris = dropTarget.value.split(/\r?\n/);
|
||||
const firstUri = uris[0];
|
||||
|
||||
if(uris.length > 1 || !Gst.uri_is_valid(firstUri))
|
||||
return;
|
||||
|
||||
/* Check if user is dragging a YouTube link */
|
||||
const [isYouTubeUri, videoId] = YouTube.checkYouTubeUri(firstUri);
|
||||
if(!isYouTubeUri) return;
|
||||
|
||||
/* Since this is a YouTube video,
|
||||
* create YT client if it was not created yet */
|
||||
if(!this.player.ytClient)
|
||||
this.player.ytClient = new YouTube.YouTubeClient();
|
||||
|
||||
const { ytClient } = this.player;
|
||||
|
||||
/* Speed up things by prefetching new video info before drop */
|
||||
if(
|
||||
!ytClient.compareLastVideoId(videoId)
|
||||
&& ytClient.downloadingVideoId !== videoId
|
||||
)
|
||||
ytClient.getVideoInfoPromise(videoId).catch(debug);
|
||||
}
|
||||
|
||||
_onDataMotion(dropTarget, x, y)
|
||||
{
|
||||
return Gdk.DragAction.MOVE;
|
||||
|
1003
src/youtube.js
1003
src/youtube.js
File diff suppressed because it is too large
Load Diff
@@ -1,85 +0,0 @@
|
||||
var QualityType = {
|
||||
0: 'normal',
|
||||
1: 'hfr',
|
||||
};
|
||||
|
||||
const Itags = {
|
||||
video: {
|
||||
h264: {
|
||||
normal: {
|
||||
240: 133,
|
||||
360: 134,
|
||||
480: 135,
|
||||
720: 136,
|
||||
1080: 137,
|
||||
},
|
||||
hfr: {
|
||||
720: 298,
|
||||
1080: 299,
|
||||
},
|
||||
},
|
||||
},
|
||||
audio: {
|
||||
aac: [140],
|
||||
opus: [249, 250, 251],
|
||||
},
|
||||
combined: {
|
||||
360: 18,
|
||||
720: 22,
|
||||
},
|
||||
hls: {
|
||||
240: 92,
|
||||
360: 93,
|
||||
480: 94,
|
||||
720: 95,
|
||||
1080: 96,
|
||||
}
|
||||
};
|
||||
|
||||
function _appendItagArray(arr, opts, formats)
|
||||
{
|
||||
const keys = Object.keys(formats);
|
||||
|
||||
for(let fmt of keys) {
|
||||
arr.push(formats[fmt]);
|
||||
|
||||
if(
|
||||
fmt >= opts.height
|
||||
|| Math.floor(fmt * 16 / 9) >= opts.width
|
||||
)
|
||||
break;
|
||||
}
|
||||
|
||||
return arr;
|
||||
}
|
||||
|
||||
function getDashItags(opts)
|
||||
{
|
||||
const allowed = {
|
||||
video: [],
|
||||
audio: (opts.codec === 'h264')
|
||||
? Itags.audio.aac
|
||||
: Itags.audio.opus
|
||||
};
|
||||
const types = Object.keys(Itags.video[opts.codec]);
|
||||
|
||||
for(let type of types) {
|
||||
const formats = Itags.video[opts.codec][type];
|
||||
_appendItagArray(allowed.video, opts, formats);
|
||||
|
||||
if(type === opts.type)
|
||||
break;
|
||||
}
|
||||
|
||||
return allowed;
|
||||
}
|
||||
|
||||
function getCombinedItags(opts)
|
||||
{
|
||||
return _appendItagArray([], opts, Itags.combined);
|
||||
}
|
||||
|
||||
function getHLSItags(opts)
|
||||
{
|
||||
return _appendItagArray([], opts, Itags.hls);
|
||||
}
|
@@ -197,8 +197,8 @@
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwPreferencesGroup">
|
||||
<property name="title" translatable="no">YouTube</property>
|
||||
<object class="AdwPreferencesGroup" id="gtuber_group">
|
||||
<property name="title" translatable="no">Gtuber</property>
|
||||
<child>
|
||||
<object class="ClapperPrefsSwitch">
|
||||
<property name="title" translatable="yes">Prefer adaptive streaming</property>
|
||||
|
Reference in New Issue
Block a user