Closed Trung0246 closed 2 years ago
I think the problem is getting the info json in the first place. Here might be the solution: https://stackoverflow.com/a/68711617
edit: nevermind, you are right. Still have no clue how I'd patch this in. I'm too little of a programmer.
edit2: fixed it:
async function getStoryboard(videoId) {
/*
let result = await fetch("https://www.youtube.com/get_video_info?video_id="+videoId+"&html5=1&c=TVHTML5&cver=6.20180913")
let text = await result.text();
let videoInfo = new URLSearchParams(text);
let player_response = videoInfo.get("player_response");
//let player_response = ytplayer.config.args.player_response;
let details = JSON.parse(player_response)
//let length = parseInt(details.videoDetails.lengthSeconds);
*/
let length = parseInt(window.ytInitialPlayerResponse.videoDetails.lengthSeconds);
//let spec = details.storyboards.playerStoryboardSpecRenderer.spec;
let spec = window.ytInitialPlayerResponse.storyboards.playerStoryboardSpecRenderer.spec;
let parts = spec.split("|");
let baseUrl = parts.shift();
...
Maybe I'll figure out how to do a PR and cleanup tomorrow
For embed youtube video I don't think ytInitialPlayerResponse
really exist so this is my current fix
let spec;
if (!window.ytInitialPlayerResponse) {
// for embed youtube
let fetch_data = await fetch(`https://www.youtube.com/youtubei/v1/player?key=${window.yt.config_.INNERTUBE_API_KEY}`, {
"headers": {
"content-type": "application/json",
},
"body": JSON.stringify({
"videoId": videoId,
"context": {
"client": {
"clientName": "WEB_EMBEDDED_PLAYER",
"clientVersion": "1.20210629.1.0",
},
},
}),
"method": "POST",
});
fetch_data = await fetch_data.json();
spec = fetch_data.storyboards ? (fetch_data.storyboards.playerStoryboardSpecRenderer || fetch_data.storyboards.playerLiveStoryboardSpecRenderer) : null;
if (spec) {
spec = spec.spec;
} else {
throw `Cannot find storyboard url, the video ${videoId} likely only have a static image`;
}
} else {
spec = window.ytInitialPlayerResponse.storyboards.playerStoryboardSpecRenderer?.spec || window.ytInitialPlayerResponse.storyboards.playerLiveStoryboardSpecRenderer.spec;
}
Hey @Trung0246 and @cougarten, thanks for looking into this! :) Do you have a version that works for you currently, by any chance? I'd love to have a copy!
// ==UserScript==
// @name Timelens for YouTube
// @description Adds a Timelens-like interface to YouTube (see https://timelens.io)
// @namespace https://github.com/timelens
// @include https://www.youtube.com/*
// @version 1.2.1
// @grant none
// @run-at document-end
// ==/UserScript==
"use strict";
// get api key by ytcfg.data_.INNERTUBE_API_KEY
let intervalClock;
async function getStoryboard(videoId) {
/*
let result = await fetch("https://www.youtube.com/get_video_info?video_id="+videoId+"&asv=3&el=detailpage&hl=en_US")
let text = await result.text();
let videoInfo = new URLSearchParams(text);
let player_response = videoInfo.get("player_response");
//let player_response = ytplayer.config.args.player_response;
let details = JSON.parse(player_response)
let length = parseInt(details.videoDetails.lengthSeconds);
if (!details.storyboards.playerStoryboardSpecRenderer) {
clearInterval(intervalClock);
throw "Livestream doesn't need timelens";
}
//*/
let spec;
if (!window.ytInitialPlayerResponse) {
// for embed youtube
let fetch_data = await fetch(`https://www.youtube.com/youtubei/v1/player?key=${window.yt.config_.INNERTUBE_API_KEY}`, {
"headers": {
"content-type": "application/json",
},
"body": JSON.stringify({
"videoId": videoId,
"context": {
"client": {
"clientName": "WEB_EMBEDDED_PLAYER",
"clientVersion": "1.20210629.1.0",
},
},
}),
"method": "POST",
});
fetch_data = await fetch_data.json();
spec = fetch_data.storyboards ? (fetch_data.storyboards.playerStoryboardSpecRenderer || fetch_data.storyboards.playerLiveStoryboardSpecRenderer) : null;
if (spec) {
spec = spec.spec;
} else {
window.clearInterval(intervalClock);
throw `Cannot find storyboard url, the video ${videoId} likely only have a static image (2)`;
}
} else {
spec = window.ytInitialPlayerResponse.storyboards.playerStoryboardSpecRenderer?.spec || window.ytInitialPlayerResponse.storyboards.playerLiveStoryboardSpecRenderer.spec;
}
console.log("Found storyboard url: ", spec);
window.clearInterval(intervalClock);
// old: details.storyboards.playerStoryboardSpecRenderer.spec;
// or window.ytplayer.config.args.raw_player_response.storyboards.playerStoryboardSpecRenderer.spec
let parts = spec.split("|");
let baseUrl = parts.shift();
let levels = parts.map((part, i) => {
let params = part.split("#");
let width = parseInt(params[0]);
let height = parseInt(params[1]);
let count = parseInt(params[2]);
let cols = parseInt(params[3]);
let rows = parseInt(params[4]);
let unknown = params[5];
let replacement = params[6];
let sigh = params[7];
if (replacement == "default") {
replacement = "$M";
}
let url = baseUrl.replace(/\$L/, "" + i).replace(/\$N/, replacement) + "&sigh=" + sigh;
return {
width,
height,
count,
cols,
rows,
unknown,
sigh,
url,
};
});
return {
length,
levels,
};
}
function getVideoId() {
let match = location.href.match(/^.*(?:(?:youtu\.be\/|v\/|vi\/|u\/\w\/|embed\/)|(?:(?:watch)?\?v(?:i)?=|\&v(?:i)?=))([^#\&\?]*).*/);
if (match) return match[1];
return null;
//return (new URLSearchParams(location.search)).get("v");
}
function range(n) {
return [...Array(n).keys()];
}
function loadImage(url) {
return new Promise((resolve, reject) => {
var img = new Image;
img.crossOrigin = "anonymous";
img.onload = () => resolve(img);
img.onerror = reject;
img.src = url;
});
}
async function loadSheet(params, sheet, ctx) {
let sheetSize = params.cols * params.rows;
let end = Math.min(params.count, sheet * sheetSize + sheetSize);
let image = await loadImage(params.url.replace(/\$M/, "" + sheet));
for (let i = sheet * sheetSize; i < end; i++) {
let absrow = parseInt(i / params.cols);
let sheet = parseInt(absrow / params.rows);
let row = absrow % params.rows;
let col = i % params.cols;
let sx = col * params.width;
let sy = row * params.height;
ctx.drawImage(image, sx, sy, params.width, params.height, i, 0, 1, params.height);
}
}
async function getTimelens(videoId) {
let storyboard = await getStoryboard(videoId);
let params = storyboard.levels.pop();
if (!params) {
window.clearInterval(intervalClock);
throw `Cannot find storyboard url, the video ${videoId} likely only have a static image (1)`;
}
let canvas = document.createElement("canvas");
canvas.dataset.videoId = videoId;
canvas.width = params.count;
canvas.height = params.height;
let ctx = canvas.getContext("2d");
let sheetCount = Math.ceil(params.count / params.cols / params.rows);
range(sheetCount).forEach(i => loadSheet(params, i, ctx));
return canvas;
}
async function insertTimelens() {
let videoId = getVideoId();
if (!videoId) return;
let old = document.getElementById("timelens");
if ((!old) || old.dataset.videoId != videoId) {
let bar = document.querySelector(".ytp-progress-bar");
let canvas = await getTimelens(videoId);
canvas.id = "timelens";
while (old = document.getElementById("timelens")) {
old.parentNode.removeChild(old);
}
bar.appendChild(canvas);
}
}
function ProgressBarObserver() {
let timeLeft = document.querySelector(".ytp-bound-time-left");
let timeRight = document.querySelector(".ytp-bound-time-right");
if (!(timeLeft && timeRight)) {
throw "No video loaded yet";
}
function getTime(element) {
let values = element.textContent.split(":");
let seconds = parseInt(values.pop());
if (values.length) {
seconds += parseInt(values.pop()) * 60;
}
if (values.length) {
seconds += parseInt(values.pop()) * 60 * 60;
}
if (values.length) {
seconds += parseInt(values.pop()) * 60 * 60 * 24;
}
return seconds;
}
function getProgressParams() {
let length = parseInt(document.querySelector(".ytp-progress-bar").getAttribute("aria-valuemax"));
let start = getTime(timeLeft);
let end = getTime(timeRight);
return {
length,
start,
end
};
}
let observer = new MutationObserver(() => {
let timelens = document.getElementById("timelens");
if (!timelens) return;
let {
start,
end,
length
} = getProgressParams();
let offset = start / length;
let zoom = length / (end - start);
timelens.style.transform = `scaleX(${zoom}) translateX(${-offset*100}%)`;
});
observer.observe(timeLeft, {
characterData: true,
childList: true,
subtree: true,
});
observer.observe(timeRight, {
characterData: true,
childList: true,
subtree: true,
});
this.destroy = function() {
observer.disconnect();
}
}
var progressObserver = null, flag = false;
intervalClock = setInterval(function() {
insertTimelens().catch(function () {
clearInterval(intervalClock);
console.error(...arguments);
flag = true;
});
if (flag) return;
if (!progressObserver) {
try {
progressObserver = new ProgressBarObserver();
} catch (e) {}
}
}, 2000);
var style = document.head.appendChild(document.createElement("style"));
style.type = "text/css";
style.textContent = `
#timelens {
position: absolute;
left: 0;
bottom: 100%;
width: 100%;
height: 40px;
opacity: 0;
transition: opacity 0.2s;
image-rendering: smooth;
transform-origin: 0 0;
}
.ytp-progress-bar:hover #timelens,
.ytp-progress-bar-container.ytp-drag #timelens {
opacity: 1;
display: block;
}
.ytp-tooltip.ytp-preview {
transform: translateY(-35px);
}
`;
@blinry my current patched version. Pretty much spaghetti code and too lazy to modify since last year 😅
Thanks a lot! :)
Hi, instead of using
get_video_info
, I've suggest we use global variableytInitialPlayerResponse.storyboards.playerStoryboardSpecRenderer.spec
orytplayer.config.args.raw_player_response.storyboards.playerStoryboardSpecRenderer.spec
to get url needed to fetch the storyboard thumbnail: