mirror of
https://git.sr.ht/~cadence/cloudtube
synced 2026-03-02 02:31:35 +00:00
First working video page
This commit is contained in:
parent
23a7da45d3
commit
cbc3a2bf67
21 changed files with 3935 additions and 78 deletions
BIN
html/static/fonts/bariol.woff
Executable file
BIN
html/static/fonts/bariol.woff
Executable file
Binary file not shown.
3
html/static/images/arrow-down-wide.svg
Normal file
3
html/static/images/arrow-down-wide.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 5.821 2.117" height="8" width="22">
|
||||
<path d="M1.587.53l.53.528.529-.529h.264v.265l-.793.793-.794-.793V.529z" fill="#bbb" paint-order="markers stroke fill"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 221 B |
7
html/static/images/search.svg
Normal file
7
html/static/images/search.svg
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 4.531 4.531" height="17.127" width="17.124">
|
||||
<g fill="#bbb">
|
||||
<path paint-order="markers stroke fill" d="M1.431 2.624l.477.477-1.43 1.43L0 4.055z"/>
|
||||
<path paint-order="markers stroke fill" d="M2.092 2.138l.302.302-1.43 1.43-.303-.302z"/>
|
||||
<path d="M4.117.413a1.417 1.417 0 0 0-2.003 2.003 1.419 1.419 0 0 0 2.003 0 1.419 1.419 0 0 0 0-2.003zm-.214.214c.436.437.436 1.14 0 1.576a1.112 1.112 0 0 1-1.575 0 1.11 1.11 0 0 1 0-1.574A1.11 1.11 0 0 1 3.904.628z" style="line-height:normal;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000;text-transform:none;text-orientation:mixed;shape-padding:0;isolation:auto;mix-blend-mode:normal" color="#000" font-weight="400" font-family="sans-serif" white-space="normal" overflow="visible" paint-order="markers stroke fill"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
137
html/static/js/elemjs/elemjs.js
Normal file
137
html/static/js/elemjs/elemjs.js
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
/**
|
||||
* Shortcut for querySelector.
|
||||
* @template {HTMLElement} T
|
||||
* @returns {T}
|
||||
*/
|
||||
const q = s => document.querySelector(s);
|
||||
/**
|
||||
* Shortcut for querySelectorAll.
|
||||
* @template {HTMLElement} T
|
||||
* @returns {T[]}
|
||||
*/
|
||||
const qa = s => document.querySelectorAll(s);
|
||||
|
||||
/**
|
||||
* An easier, chainable, object-oriented way to create and update elements
|
||||
* and children according to related data. Subclass ElemJS to create useful,
|
||||
* advanced data managers, or just use it inline to quickly make a custom element.
|
||||
*/
|
||||
class ElemJS {
|
||||
constructor(type) {
|
||||
if (type instanceof HTMLElement) {
|
||||
// If passed an existing element, bind to it
|
||||
this.bind(type);
|
||||
} else if (typeof type === "string") {
|
||||
// Otherwise, create a new detached element to bind to
|
||||
this.bind(document.createElement(type));
|
||||
} else {
|
||||
throw new Error("Cannot create an element of type ${type}")
|
||||
}
|
||||
this.children = [];
|
||||
}
|
||||
|
||||
/** Bind this construct to an existing element on the page. */
|
||||
bind(element) {
|
||||
this.element = element;
|
||||
this.element.js = this;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Add a class. */
|
||||
class() {
|
||||
for (let name of arguments) if (name) this.element.classList.add(name);
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Remove a class. */
|
||||
removeClass() {
|
||||
for (let name of arguments) if (name) this.element.classList.remove(name);
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Set a JS property on the element. */
|
||||
direct(name, value) {
|
||||
if (name) this.element[name] = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Set an attribute on the element. */
|
||||
attribute(name, value) {
|
||||
if (name) this.element.setAttribute(name, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Set a style on the element. */
|
||||
style(name, value) {
|
||||
if (name) this.element.style[name] = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Set the element's ID. */
|
||||
id(name) {
|
||||
if (name) this.element.id = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Attach a callback function to an event on the element. */
|
||||
on(name, callback) {
|
||||
this.element.addEventListener(name, callback);
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Set the element's text. */
|
||||
text(name) {
|
||||
this.element.innerText = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Create a text node and add it to the element. */
|
||||
addText(name) {
|
||||
const node = document.createTextNode(name);
|
||||
this.element.appendChild(node);
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Set the element's HTML content. */
|
||||
html(name) {
|
||||
this.element.innerHTML = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add children to the element.
|
||||
* Children can either be an instance of ElemJS, in
|
||||
* which case the element will be appended as a child,
|
||||
* or a string, in which case the string will be added as a text node.
|
||||
* Each child should be a parameter to this method.
|
||||
*/
|
||||
child(...children) {
|
||||
for (const toAdd of children) {
|
||||
if (typeof toAdd === "object" && toAdd !== null) {
|
||||
// Should be an instance of ElemJS, so append as child
|
||||
toAdd.parent = this;
|
||||
this.element.appendChild(toAdd.element);
|
||||
this.children.push(toAdd);
|
||||
} else if (typeof toAdd === "string") {
|
||||
// Is a string, so add as text node
|
||||
this.addText(toAdd);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all children from the element.
|
||||
*/
|
||||
clearChildren() {
|
||||
this.children.length = 0;
|
||||
while (this.element.lastChild) this.element.removeChild(this.element.lastChild);
|
||||
}
|
||||
}
|
||||
|
||||
/** Shortcut for `new ElemJS`. */
|
||||
function ejs(tag) {
|
||||
return new ElemJS(tag);
|
||||
}
|
||||
|
||||
export {q, qa, ElemJS, ejs};
|
||||
|
|
@ -1,78 +0,0 @@
|
|||
/** @returns {HTMLElement} */
|
||||
export function q(s) {
|
||||
return document.querySelector(s)
|
||||
}
|
||||
|
||||
export class ElemJS {
|
||||
constructor(type) {
|
||||
if (type instanceof HTMLElement) this.bind(type)
|
||||
else this.bind(document.createElement(type))
|
||||
this.children = [];
|
||||
}
|
||||
bind(element) {
|
||||
/** @type {HTMLElement} */
|
||||
this.element = element
|
||||
// @ts-ignore
|
||||
this.element.js = this
|
||||
return this
|
||||
}
|
||||
class() {
|
||||
for (let name of arguments) if (name) this.element.classList.add(name);
|
||||
return this;
|
||||
}
|
||||
removeClass() {
|
||||
for (let name of arguments) if (name) this.element.classList.remove(name);
|
||||
return this;
|
||||
}
|
||||
direct(name, value) {
|
||||
if (name) this.element[name] = value;
|
||||
return this;
|
||||
}
|
||||
attribute(name, value) {
|
||||
if (name) this.element.setAttribute(name, value);
|
||||
return this;
|
||||
}
|
||||
style(name, value) {
|
||||
if (name) this.element.style[name] = value;
|
||||
return this;
|
||||
}
|
||||
id(name) {
|
||||
if (name) this.element.id = name;
|
||||
return this;
|
||||
}
|
||||
text(name) {
|
||||
this.element.innerText = name;
|
||||
return this;
|
||||
}
|
||||
addText(name) {
|
||||
const node = document.createTextNode(name)
|
||||
this.element.appendChild(node)
|
||||
return this
|
||||
}
|
||||
html(name) {
|
||||
this.element.innerHTML = name;
|
||||
return this;
|
||||
}
|
||||
event(name, callback) {
|
||||
this.element.addEventListener(name, event => callback(event))
|
||||
}
|
||||
child(toAdd, position) {
|
||||
if (typeof(toAdd) == "object") {
|
||||
toAdd.parent = this;
|
||||
if (typeof(position) == "number" && position >= 0) {
|
||||
this.element.insertBefore(toAdd.element, this.element.children[position]);
|
||||
this.children.splice(position, 0, toAdd);
|
||||
} else {
|
||||
this.element.appendChild(toAdd.element);
|
||||
this.children.push(toAdd);
|
||||
}
|
||||
} else if (typeof toAdd === "string") {
|
||||
this.text(toAdd)
|
||||
}
|
||||
return this;
|
||||
}
|
||||
clearChildren() {
|
||||
this.children.length = 0;
|
||||
while (this.element.lastChild) this.element.removeChild(this.element.lastChild);
|
||||
}
|
||||
}
|
||||
9
html/static/js/focus.js
Normal file
9
html/static/js/focus.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
document.addEventListener("mousedown", () => {
|
||||
document.body.classList.remove("show-focus")
|
||||
})
|
||||
|
||||
document.addEventListener("keydown", event => {
|
||||
if (event.key === "Tab") {
|
||||
document.body.classList.add("show-focus")
|
||||
}
|
||||
})
|
||||
117
html/static/js/player.js
Normal file
117
html/static/js/player.js
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
import {q, ElemJS} from "/static/js/elemjs/elemjs.js"
|
||||
|
||||
const video = q("#video")
|
||||
const audio = q("#audio")
|
||||
|
||||
const videoFormats = new Map()
|
||||
const audioFormats = new Map()
|
||||
for (const f of [].concat(
|
||||
data.formatStreams.map(f => (f.isAdaptive = false, f)),
|
||||
data.adaptiveFormats.map(f => (f.isAdaptive = true, f))
|
||||
)) {
|
||||
if (f.type.startsWith("video")) {
|
||||
videoFormats.set(f.itag, f)
|
||||
} else {
|
||||
audioFormats.set(f.itag, f)
|
||||
}
|
||||
}
|
||||
|
||||
function getBestAudioFormat() {
|
||||
let best = null
|
||||
for (const f of audioFormats.values()) {
|
||||
if (best === null || f.bitrate > best.bitrate) {
|
||||
best = f
|
||||
}
|
||||
}
|
||||
return best
|
||||
}
|
||||
|
||||
class FormatLoader {
|
||||
constructor() {
|
||||
this.npv = videoFormats.get(q("#video").getAttribute("data-itag"))
|
||||
this.npa = null
|
||||
}
|
||||
|
||||
play(itag) {
|
||||
this.npv = videoFormats.get(itag)
|
||||
if (this.npv.isAdaptive) {
|
||||
this.npa = getBestAudioFormat()
|
||||
} else {
|
||||
this.npa = null
|
||||
}
|
||||
this.update()
|
||||
}
|
||||
|
||||
update() {
|
||||
const lastTime = video.currentTime
|
||||
video.src = this.npv.url
|
||||
video.currentTime = lastTime
|
||||
if (this.npa) {
|
||||
audio.src = this.npa.url
|
||||
audio.currentTime = lastTime
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const formatLoader = new FormatLoader()
|
||||
|
||||
class QualitySelect extends ElemJS {
|
||||
constructor() {
|
||||
super(q("#quality-select"))
|
||||
this.on("input", this.onInput.bind(this))
|
||||
}
|
||||
|
||||
onInput() {
|
||||
const itag = this.element.value
|
||||
formatLoader.play(itag)
|
||||
}
|
||||
}
|
||||
|
||||
const qualitySelect = new QualitySelect()
|
||||
|
||||
function playbackIntervention(event) {
|
||||
console.log(event.target.tagName.toLowerCase(), event.type)
|
||||
if (audio.src) {
|
||||
let target = event.target
|
||||
let targetName = target.tagName.toLowerCase()
|
||||
let other = (event.target === video ? audio : video)
|
||||
switch (event.type) {
|
||||
case "durationchange":
|
||||
target.ready = false;
|
||||
break;
|
||||
case "seeked":
|
||||
target.ready = false;
|
||||
target.pause();
|
||||
other.currentTime = target.currentTime;
|
||||
break;
|
||||
case "play":
|
||||
other.currentTime = target.currentTime;
|
||||
other.play();
|
||||
break;
|
||||
case "pause":
|
||||
other.currentTime = target.currentTime;
|
||||
other.pause();
|
||||
case "playing":
|
||||
other.currentTime = target.currentTime;
|
||||
break;
|
||||
case "ratechange":
|
||||
other.rate = target.rate;
|
||||
break;
|
||||
// case "stalled":
|
||||
// case "waiting":
|
||||
// target.pause();
|
||||
// break;
|
||||
}
|
||||
} else {
|
||||
// @ts-ignore this does exist
|
||||
// if (event.type == "canplaythrough" && !video.manualPaused) video.play();
|
||||
}
|
||||
}
|
||||
|
||||
for (let eventName of ["pause", "play", "seeked"]) {
|
||||
video.addEventListener(eventName, playbackIntervention)
|
||||
}
|
||||
for (let eventName of ["canplaythrough", "waiting", "stalled"]) {
|
||||
video.addEventListener(eventName, playbackIntervention)
|
||||
audio.addEventListener(eventName, playbackIntervention)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue