Compare commits

...

23 Commits

Author SHA1 Message Date
3bd6603396 Add initial video player implementation with controls and error handling
All checks were successful
Build and release container directly from master / release (push) Successful in 5m42s
Stale issue handler / stale (push) Successful in 12s
2025-05-05 20:31:12 -04:00
5c2700f9fd Add video player with controls, hotkeys, and quality selection for DASH streams
All checks were successful
Build and release container directly from master / release (push) Successful in 5m33s
Stale issue handler / stale (push) Successful in 24s
2025-05-05 16:34:33 -04:00
16333615fa Add initial player.js with video playback and controls implementation
All checks were successful
Build and release container directly from master / release (push) Successful in 5m40s
2025-05-05 16:03:10 -04:00
3b56443825 Add initial player.js with Video.js setup and event handling
All checks were successful
Build and release container directly from master / release (push) Successful in 5m34s
2025-05-05 15:01:51 -04:00
8d3ff993dd Loggn
All checks were successful
Build and release container directly from master / release (push) Successful in 5m24s
2025-05-05 14:31:34 -04:00
7a750167eb Add video player with autoplay, DASH streaming and mobile UI support
All checks were successful
Build and release container directly from master / release (push) Successful in 5m26s
2025-05-05 14:20:21 -04:00
9a83674a42 Add video player implementation with controls, autoplay and quality settings
All checks were successful
Build and release container directly from master / release (push) Successful in 5m32s
2025-05-05 14:06:13 -04:00
91a4ac3744 Add video player functionality with controls, autoplay, and time tracking
All checks were successful
Build and release container directly from master / release (push) Successful in 11m48s
2025-05-05 13:26:42 -04:00
f22891d7b6 Add GitHub workflow for building nightly container images and player.js
All checks were successful
Build and release container directly from master / release (push) Successful in 6m2s
2025-05-05 13:14:29 -04:00
1fca61734b Add GitHub workflow to build and push nightly Docker images for AMD64 and ARM64
Some checks failed
Build and release container directly from master / release (push) Has been cancelled
2025-05-05 11:22:18 -04:00
e447d4275e Add GitHub workflow to build and push nightly Docker images for AMD64 and ARM64
Some checks failed
Build and release container directly from master / release (push) Failing after 48s
2025-05-05 11:21:11 -04:00
43cf4fabaa Add GitHub workflow to build and push nightly Docker images for AMD64 and ARM64
Some checks failed
Build and release container directly from master / release (push) Has been cancelled
2025-05-05 11:12:45 -04:00
668aff0cc2 Add GitHub workflow to build and push nightly Docker images for AMD64 and ARM64
Some checks failed
Build and release container directly from master / release (push) Failing after 29s
2025-05-05 11:11:41 -04:00
5c282637a6 ehh
Some checks failed
Build and release container directly from master / release (push) Failing after 28s
2025-05-05 11:10:56 -04:00
2ba1fe4db9 cant' start with gitea
Some checks failed
Build and release container directly from master / release (push) Failing after 34s
2025-05-05 11:09:20 -04:00
d891212545 Add GitHub workflow to build and push nightly Docker images for AMD64 and ARM64 2025-05-05 11:08:10 -04:00
f50559ac18 Add initial video player implementation with controls and features
Some checks failed
Build and release container directly from master / release (push) Failing after 34s
2025-05-05 10:56:06 -04:00
Emilien
d1bc15b8bf Release v2.20250504.0 2025-05-04 11:59:42 +02:00
Vyquos
1f028fee0f
Reflect companion secret character limit in example config comment (#5269)
Update the comments in the example config to show that the companion secret key must be exactly 16 characters long as per https://github.com/iv-org/invidious-companion/pull/81#issuecomment-2750675405.
2025-05-04 07:47:42 +00:00
absidue
2c1400c41e
Fix proxying live DASH streams (#4589) 2025-05-03 20:28:19 +00:00
Alex Maras
8fd0b82c38
feat: route to invidious companion on downloads (#5224) 2025-05-03 01:28:18 +02:00
Émilien (perso)
7579adc3a3
fix: fallback other yt clients no url found for adaptive formats (#5262) 2025-05-02 16:57:02 +02:00
efb4f5ff-1298-471a-8973-3d47447115dc
d567c6be6e
Fix minor casing issues in brand names (#5258) 2025-05-02 15:36:31 +02:00
9 changed files with 428 additions and 247 deletions

View File

@ -23,10 +23,17 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
platforms: arm64
# - name: Set up QEMU
# uses: docker/setup-qemu-action@v3
# with:
# platforms: arm64
- name: Install docker from docker webpage
run: |
curl -fsSL https://get.docker.com -o get-docker.sh
sh get-docker.sh
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
@ -34,20 +41,18 @@ jobs:
- name: Login to registry
uses: docker/login-action@v3
with:
registry: quay.io
username: ${{ secrets.QUAY_USERNAME }}
password: ${{ secrets.QUAY_PASSWORD }}
registry: gitea.ghost.tel
username: ${{ secrets.USERNAME }}
password: ${{ secrets.PASSWORD }}
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: quay.io/invidious/invidious
images: gitea.ghost.tel/knight/invidious
tags: |
type=sha,format=short,prefix={{date 'YYYY.MM.DD'}}-,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
type=raw,value=master,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
labels: |
quay.expires-after=12w
- name: Build and push Docker AMD64 image for Push Event
uses: docker/build-push-action@v5
@ -55,7 +60,6 @@ jobs:
context: .
file: docker/Dockerfile
platforms: linux/amd64
labels: ${{ steps.meta.outputs.labels }}
push: true
tags: ${{ steps.meta.outputs.tags }}
build-args: |
@ -65,23 +69,9 @@ jobs:
id: meta-arm64
uses: docker/metadata-action@v5
with:
images: quay.io/invidious/invidious
images: gitea.ghost.tel/knight/invidious
flavor: |
suffix=-arm64
tags: |
type=sha,format=short,prefix={{date 'YYYY.MM.DD'}}-,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
type=raw,value=master,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
labels: |
quay.expires-after=12w
- name: Build and push Docker ARM64 image for Push Event
uses: docker/build-push-action@v5
with:
context: .
file: docker/Dockerfile.arm64
platforms: linux/arm64/v8
labels: ${{ steps.meta-arm64.outputs.labels }}
push: true
tags: ${{ steps.meta-arm64.outputs.tags }}
build-args: |
"release=1"

View File

@ -2,6 +2,12 @@
## vX.Y.0 (future)
## v2.20250504.0
Small release with quick workaround fix for issue #4251 (Nil assertion failed).
PR: https://github.com/iv-org/invidious/issues/5263
## v2.20250314.0
### Wrap-up

View File

@ -81,9 +81,9 @@
- [Available in many languages](locales/), thanks to [our translators](#contribute)
**Data import/export**
- Import subscriptions from YouTube, NewPipe and Freetube
- Import subscriptions from YouTube, NewPipe and FreeTube
- Import watch history from YouTube and NewPipe
- Export subscriptions to NewPipe and Freetube
- Export subscriptions to NewPipe and FreeTube
- Import/Export Invidious user data
**Technical features**
@ -95,11 +95,11 @@
## Quick start
**Using invidious:**
**Using Invidious:**
- [Select a public instance from the list](https://instances.invidious.io) and start watching videos right now!
**Hosting invidious:**
**Hosting Invidious:**
- [Follow the installation instructions](https://docs.invidious.io/installation/)
@ -114,8 +114,8 @@ https://github.com/iv-org/documentation
### Extensions
We highly recommend the use of [Privacy Redirect](https://github.com/SimonBrazell/privacy-redirect#get),
a browser extension that automatically redirects Youtube URLs to any Invidious instance and replaces
embedded youtube videos on other websites with invidious.
a browser extension that automatically redirects YouTube URLs to any Invidious instance and replaces
embedded YouTube videos on other websites with Invidious.
The documentation contains a list of browser extensions that we recommended to use along with Invidious.
@ -140,7 +140,7 @@ We use [Weblate](https://weblate.org) to manage Invidious translations.
You can suggest new translations and/or correction here: https://hosted.weblate.org/engage/invidious/.
Creating an account is not required, but recommended, especially if you want to contribute regularly.
Weblate also allows you to log-in with major SSO providers like Github, Gitlab, BitBucket, Google, ...
Weblate also allows you to log-in with major SSO providers like GitHub, GitLab, BitBucket, Google, ...
## Projects using Invidious

View File

@ -1,6 +1,8 @@
'use strict';
console.log('[Invidious Debug] player.js start');
var player_data = JSON.parse(document.getElementById('player_data').textContent);
var video_data = JSON.parse(document.getElementById('video_data').textContent);
console.log('[Invidious Debug] video_data.params:', JSON.stringify(video_data.params));
var options = {
liveui: true,
@ -50,9 +52,27 @@ videojs.Vhs.xhr.beforeRequest = function(options) {
return options;
};
var player = videojs('player', options);
console.log('[Invidious Debug] Initializing videojs...');
var player;
try {
player = videojs('player', options);
console.log('[Invidious Debug] videojs initialized successfully.');
} catch (e) {
console.error('[Invidious Debug] videojs initialization FAILED:', e);
// Stop further execution if player init fails
throw e;
}
// --- Test listener for loadedmetadata ---
player.on('loadedmetadata', function() {
console.log('[Invidious Debug] Event: loadedmetadata Fired!');
});
player.on('error', function () {
var error = player.error();
console.error('[Invidious Debug] Player Error Event:', error ? error.message : 'Unknown error', 'Code:', error ? error.code : 'N/A', 'Type:', error ? error.type : 'N/A', error);
if (video_data.params.quality === 'dash') return;
var localNotDisabled = (
@ -70,6 +90,7 @@ player.on('error', function () {
return source;
}));
} else if (reloadMakesSense) {
console.log('[Invidious Debug] Player error: Attempting reload in 5 seconds.');
setTimeout(function () {
console.warn('An error occurred in the player, reloading...');
@ -86,13 +107,54 @@ player.on('error', function () {
player.playbackRate(playbackRate);
if (!paused) player.play();
}, 5000);
} else {
console.log('[Invidious Debug] Player error: No specific action taken.');
}
});
if (video_data.params.quality === 'dash') {
player.reloadSourceOnError({
errorInterval: 10
try {
player.on('error', function () {
var error = player.error();
console.error('[Invidious Debug] Player Error Event:', error ? error.message : 'Unknown error', 'Code:', error ? error.code : 'N/A', 'Type:', error ? error.type : 'N/A', error);
// Only reload if not dash or an ad is playing.
// Don't reload if the error is Source Not Supported type, as this is used when
});
console.log('[Invidious Debug] Generic player error listener attached.');
// Add listener for tech errors (might give more specific MSE/network errors)
player.ready(function() {
const tech = player.tech_;
if (tech) {
tech.on('error', function(event) {
console.error('[Invidious Debug] Tech Error Event:', event);
if (tech.error) {
console.error('[Invidious Debug] Tech Error Details:', tech.error().message, 'Code:', tech.error().code);
}
});
console.log('[Invidious Debug] Tech error listener attached.');
} else {
console.warn('[Invidious Debug] Could not attach tech error listener.');
}
});
} catch (e) {
console.error('[Invidious Debug] FAILED attaching error listeners:', e);
}
if (player.error) {
console.log('[Invidious Debug] Player error listener attached.');
}
if (video_data.params.quality === 'dash') {
console.log('[Invidious Debug] Initializing reloadSourceOnError...');
try {
player.reloadSourceOnError({
errorInterval: 10
});
console.log('[Invidious Debug] reloadSourceOnError initialized.');
} catch (e) {
console.error('[Invidious Debug] reloadSourceOnError FAILED:', e);
}
}
/**
@ -162,116 +224,6 @@ player.on('timeupdate', function () {
}
});
var shareOptions = {
socials: ['fbFeed', 'tw', 'reddit', 'email'],
get url() {
return addCurrentTimeToURL(short_url);
},
title: player_data.title,
description: player_data.description,
image: player_data.thumbnail,
get embedCode() {
// Single quotes inside here required. HTML inserted as is into value attribute of input
return "<iframe id='ivplayer' width='640' height='360' src='" +
addCurrentTimeToURL(embed_url) + "' style='border:none;'></iframe>";
}
};
if (location.pathname.startsWith('/embed/')) {
var overlay_content = '<h1><a rel="noopener" target="_blank" href="' + location.origin + '/watch?v=' + video_data.id + '">' + player_data.title + '</a></h1>';
player.overlay({
overlays: [
{ start: 'loadstart', content: overlay_content, end: 'playing', align: 'top'},
{ start: 'pause', content: overlay_content, end: 'playing', align: 'top'}
]
});
}
// Detect mobile users and initialize mobileUi for better UX
// Detection code taken from https://stackoverflow.com/a/20293441
function isMobile() {
try{ document.createEvent('TouchEvent'); return true; }
catch(e){ return false; }
}
if (isMobile()) {
player.mobileUi({ touchControls: { seekSeconds: 5 * player.playbackRate() } });
var buttons = ['playToggle', 'volumePanel', 'captionsButton'];
if (!video_data.params.listen && video_data.params.quality === 'dash') buttons.push('audioTrackButton');
if (video_data.params.listen || video_data.params.quality !== 'dash') buttons.push('qualitySelector');
// Create new control bar object for operation buttons
const ControlBar = videojs.getComponent('controlBar');
let operations_bar = new ControlBar(player, {
children: [],
playbackRates: [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0]
});
buttons.slice(1).forEach(function (child) {operations_bar.addChild(child);});
// Remove operation buttons from primary control bar
var primary_control_bar = player.getChild('controlBar');
buttons.forEach(function (child) {primary_control_bar.removeChild(child);});
var operations_bar_element = operations_bar.el();
operations_bar_element.classList.add('mobile-operations-bar');
player.addChild(operations_bar);
// Playback menu doesn't work when it's initialized outside of the primary control bar
var playback_element = document.getElementsByClassName('vjs-playback-rate')[0];
operations_bar_element.append(playback_element);
// The share and http source selector element can't be fetched till the players ready.
player.one('playing', function () {
var share_element = document.getElementsByClassName('vjs-share-control')[0];
operations_bar_element.append(share_element);
if (!video_data.params.listen && video_data.params.quality === 'dash') {
var http_source_selector = document.getElementsByClassName('vjs-http-source-selector vjs-menu-button')[0];
operations_bar_element.append(http_source_selector);
}
});
}
// Enable VR video support
if (!video_data.params.listen && video_data.vr && video_data.params.vr_mode) {
player.crossOrigin('anonymous');
switch (video_data.projection_type) {
case 'EQUIRECTANGULAR':
player.vr({projection: 'equirectangular'});
default: // Should only be 'MESH' but we'll use this as a fallback.
player.vr({projection: 'EAC'});
}
}
// Add markers
if (video_data.params.video_start > 0 || video_data.params.video_end > 0) {
var markers = [{ time: video_data.params.video_start, text: 'Start' }];
if (video_data.params.video_end < 0) {
markers.push({ time: video_data.length_seconds - 0.5, text: 'End' });
} else {
markers.push({ time: video_data.params.video_end, text: 'End' });
}
player.markers({
onMarkerReached: function (marker) {
if (marker.text === 'End')
player.loop() ? player.markers.prev('Start') : player.pause();
},
markers: markers
});
player.currentTime(video_data.params.video_start);
}
player.volume(video_data.params.volume / 100);
player.playbackRate(video_data.params.speed);
/**
* Method for getting the contents of a cookie
*
@ -351,16 +303,12 @@ if (video_data.premiere_timestamp && Math.round(new Date() / 1000) < video_data.
}
if (video_data.params.save_player_pos) {
const url = new URL(location);
const hasTimeParam = url.searchParams.has('t');
const rememberedTime = get_video_time();
let lastUpdated = 0;
if(!hasTimeParam) {
if (rememberedTime >= video_data.length_seconds - 20)
set_seconds_after_start(0);
else
set_seconds_after_start(rememberedTime);
console.log('[Invidious Debug] Setting up player position save/restore...');
var lastUpdated = 0;
var save_video_time = function(time) {
const all_video_times = get_all_video_times();
all_video_times[video_data.id] = time;
helpers.storage.set(save_player_pos_key, all_video_times);
}
player.on('timeupdate', function () {
@ -376,64 +324,138 @@ if (video_data.params.save_player_pos) {
else remove_all_video_times();
if (video_data.params.autoplay) {
console.log('[Invidious Debug] Handling autoplay setup...');
var bpb = player.getChild('bigPlayButton');
bpb.hide();
player.ready(function () {
console.log('[Invidious Debug] Player ready for autoplay.');
new Promise(function (resolve, reject) {
setTimeout(function () {resolve(1);}, 1);
}).then(function (result) {
console.log('[Invidious Debug] Attempting autoplay...');
console.log('[Invidious Debug] Calling player.play() for autoplay...');
var promise = player.play();
if (promise !== undefined) {
promise.then(function () {
console.log('[Invidious Debug] Autoplay successful.');
}).catch(function (error) {
console.error('[Invidious Debug] Autoplay FAILED:', error);
bpb.show();
});
} else {
console.log('[Invidious Debug] Autoplay started (no promise returned).');
}
});
});
}
if (!video_data.params.listen && video_data.params.quality === 'dash') {
player.httpSourceSelector();
console.log('[Invidious Debug] Initializing httpSourceSelector...');
console.log('[Invidious Debug] Player sources BEFORE httpSourceSelector init:', JSON.stringify(player.currentSources()));
if (video_data.params.quality_dash !== 'auto') {
// --- START Pre-Plugin Environment Check ---
console.log('[Invidious Debug] Checking environment before httpSourceSelector call:');
console.log('[Invidious Debug] - typeof window.fetch:', typeof window.fetch);
console.log('[Invidious Debug] - typeof window.XMLHttpRequest:', typeof window.XMLHttpRequest);
console.log('[Invidious Debug] - typeof window.Promise:', typeof window.Promise);
console.log('[Invidious Debug] - typeof window.URL:', typeof window.URL);
console.log('[Invidious Debug] - typeof window.DOMParser:', typeof window.DOMParser);
console.log('[Invidious Debug] - videojs.options:', JSON.stringify(videojs.options));
// --- END Pre-Plugin Environment Check ---
try {
player.httpSourceSelector(); // This triggers manifest fetch & parsing
console.log('[Invidious Debug] httpSourceSelector initialized (call successful).'); // Log after call
console.log('[Invidious Debug] Player sources AFTER httpSourceSelector init call:', JSON.stringify(player.currentSources())); // Sources might not update yet
// Add listener for source changes after httpSourceSelector
player.on('sourceset', function() {
console.log('[Invidious Debug] Event: sourceset triggered. Current sources:', JSON.stringify(player.currentSources())); // Log when sources actually change
});
console.log('[Invidious Debug] sourceset listener attached.'); // Added
// Listen for potential errors during quality level processing
player.ready(function () {
player.on('loadedmetadata', function () {
const qualityLevels = Array.from(player.qualityLevels()).sort(function (a, b) {return a.height - b.height;});
let targetQualityLevel;
switch (video_data.params.quality_dash) {
case 'best':
targetQualityLevel = qualityLevels.length - 1;
break;
case 'worst':
targetQualityLevel = 0;
break;
default:
const targetHeight = parseInt(video_data.params.quality_dash);
for (let i = 0; i < qualityLevels.length; i++) {
if (qualityLevels[i].height <= targetHeight)
targetQualityLevel = i;
else
// Add listener for quality level errors
const qualityLevels = player.qualityLevels();
if (qualityLevels) {
qualityLevels.on('error', function(event) {
console.error('[Invidious Debug] QualityLevels Error Event:', event);
});
console.log('[Invidious Debug] QualityLevels error listener attached.'); // Added
// Log initial quality levels once metadata is loaded
player.on('loadedmetadata', function() {
try {
console.log('[Invidious Debug] Event: loadedmetadata triggered.'); // Added
const levels = Array.from(player.qualityLevels());
// Log detailed info about each level
console.log('[Invidious Debug] Quality levels after loadedmetadata:', JSON.stringify(levels.map(l => ({id: l.id, width: l.width, height: l.height, bitrate: l.bitrate, enabled: l.enabled}))));
} catch (e) {
console.error('[Invidious Debug] Error logging/processing quality levels:', e); // Added
}
});
console.log('[Invidious Debug] loadedmetadata listener attached for quality level logging.'); // Added
} else {
console.warn('[Invidious Debug] Could not attach QualityLevels error listener or get levels.'); // Added
}
});
if (video_data.params.quality_dash !== 'auto') {
console.log('[Invidious Debug] Setting DASH quality:', video_data.params.quality_dash);
player.ready(function () {
player.on('loadedmetadata', function () {
try {
const qualityLevels = Array.from(player.qualityLevels()).sort(function (a, b) {return a.height - b.height;});
let targetQualityLevel;
switch (video_data.params.quality_dash) {
case 'best':
targetQualityLevel = qualityLevels.length - 1;
break;
case 'worst':
targetQualityLevel = 0;
break;
default:
const targetHeight = parseInt(video_data.params.quality_dash);
for (let i = 0; i < qualityLevels.length; i++) {
if (qualityLevels[i].height <= targetHeight)
targetQualityLevel = i;
else
break;
}
}
}
qualityLevels.forEach(function (level, index) {
level.enabled = (index === targetQualityLevel);
qualityLevels.forEach(function (level, index) {
level.enabled = (index === targetQualityLevel);
});
console.log('[Invidious Debug] DASH quality levels set.');
} catch (e) {
console.error('[Invidious Debug] Error setting DASH quality levels:', e);
}
});
});
});
}
} catch (e) {
console.error('[Invidious Debug] httpSourceSelector FAILED (call threw error):', e); // Log if the call itself fails
}
}
player.vttThumbnails({
src: '/api/v1/storyboards/' + video_data.id + '?height=90',
showTimestamp: true
});
console.log('[Invidious Debug] Initializing vttThumbnails...');
try {
player.vttThumbnails({
src: '/api/v1/storyboards/' + video_data.id + '?height=90',
showTimestamp: true
});
console.log('[Invidious Debug] vttThumbnails initialized.');
} catch (e) {
console.error('[Invidious Debug] vttThumbnails FAILED:', e);
}
// Enable annotations
if (!video_data.params.listen && video_data.params.annotations) {
console.log('[Invidious Debug] Initializing annotations...');
addEventListener('load', function (e) {
addEventListener('__ar_annotation_click', function (e) {
const url = e.detail.url,
@ -468,65 +490,215 @@ if (!video_data.params.listen && video_data.params.annotations) {
} else {
player.youtubeAnnotationsPlugin({ annotationXml: response, videoContainer: video_container });
}
console.log('[Invidious Debug] Annotations initialized.');
}
});
});
}
function change_volume(delta) {
const curVolume = player.volume();
let newVolume = curVolume + delta;
newVolume = helpers.clamp(newVolume, 0, 1);
player.volume(newVolume);
player.on('play', function() {
console.log('[Invidious Debug] Event: play triggered. Current source:', JSON.stringify(player.currentSource()));
});
player.on('playing', function() {
console.log('[Invidious Debug] Event: playing triggered. Playback has started.');
});
player.on('waiting', function() {
console.log('[Invidious Debug] Event: waiting triggered. Buffering or waiting for data.');
});
var shareOptions = {
socials: ['fbFeed', 'tw', 'reddit', 'email'],
get url() {
return addCurrentTimeToURL(short_url);
},
title: player_data.title,
description: player_data.description,
image: player_data.thumbnail,
get embedCode() {
// Single quotes inside here required. HTML inserted as is into value attribute of input
return "<iframe id='ivplayer' width='640' height='360' src='" +
addCurrentTimeToURL(embed_url) + "' style='border:none;'></iframe>";
}
};
if (location.pathname.startsWith('/embed/')) {
console.log('[Invidious Debug] Initializing overlay...');
try {
var overlay_content = '<h1><a rel="noopener" target="_blank" href="' + location.origin + '/watch?v=' + video_data.id + '">' + player_data.title + '</a></h1>';
player.overlay({
overlays: [
{ start: 'loadstart', content: overlay_content, end: 'playing', align: 'top'},
{ start: 'pause', content: overlay_content, end: 'playing', align: 'top'}
]
});
console.log('[Invidious Debug] Overlay initialized.');
} catch (e) {
console.error('[Invidious Debug] Overlay FAILED:', e);
}
}
function toggle_muted() {
player.muted(!player.muted());
// Detect mobile users and initialize mobileUi for better UX
// Detection code taken from https://stackoverflow.com/a/20293441
function isMobile() {
return typeof window.orientation !== 'undefined' || navigator.userAgent.indexOf('IEMobile') !== -1;
}
function skip_seconds(delta) {
const duration = player.duration();
const curTime = player.currentTime();
let newTime = curTime + delta;
newTime = helpers.clamp(newTime, 0, duration);
player.currentTime(newTime);
if (isMobile()) {
console.log('[Invidious Debug] Mobile detected, initializing mobile UI...');
try {
player.mobileUi({ touchControls: { seekSeconds: 5, tapTimeout: 200, disableOnEnd: true } });
console.log('[Invidious Debug] Mobile UI initialized.');
} catch (e) {
console.error('[Invidious Debug] Mobile UI FAILED:', e);
}
}
function set_seconds_after_start(delta) {
const start = video_data.params.video_start;
player.currentTime(start + delta);
if (player.markers && !video_data.params.listen && video_data.params.chapters) {
console.log('[Invidious Debug] Initializing markers...');
var markers = [];
for (var i = 0; i < video_data.params.chapters.length; i++) {
var marker = video_data.params.chapters[i];
markers.push({
time: marker.start,
text: marker.title,
});
}
if (markers.length > 1 && markers[0].time === 0 && markers[0].text === 'Intro') {
markers.shift();
}
markers.sort((a,b) => (a.time > b.time) ? 1 : ((b.time > a.time) ? -1 : 0));
var unique_markers = [];
var last_time = -1;
for (var i = 0; i < markers.length; i++) {
var marker = markers[i];
if (marker.time <= last_time) {
continue;
}
unique_markers.push(marker);
last_time = marker.time;
}
markers = unique_markers;
// Add "end" time information to each marker
for (var i = 0; i < markers.length; i++) {
var marker = markers[i];
if (i < markers.length - 1) {
marker.end = markers[i+1].time - 0.001;
} else {
marker.end = player.duration();
}
}
player.markers.removeAll();
// Callback function when a marker is reached
function onMarkerReached(marker) {
//console.log("marker reached: " + marker.text + " at " + marker.time);
}
try {
player.markers({
onMarkerReached: onMarkerReached,
markers: markers
});
console.log('[Invidious Debug] Markers initialized.');
} catch (e) {
console.error('[Invidious Debug] Markers FAILED:', e);
}
player.currentTime(video_data.params.video_start);
}
player.volume(video_data.params.volume / 100);
player.playbackRate(video_data.params.speed);
console.log('[Invidious Debug] Volume and playback rate set.');
if (player.share) {
console.log('[Invidious Debug] Initializing share plugin...');
try {
player.share(shareOptions);
console.log('[Invidious Debug] Share plugin initialized.');
} catch(e) {
console.error('[Invidious Debug] Share plugin FAILED:', e);
}
}
/**
* Method for saving the current video time
*
* @param {number} seconds
*/
function save_video_time(seconds) {
const all_video_times = get_all_video_times();
all_video_times[video_data.id] = seconds;
helpers.storage.set(save_player_pos_key, all_video_times);
}
/**
* Method for getting the saved video time
*
* @returns {number}
*/
function get_video_time() {
return get_all_video_times()[video_data.id] || 0;
}
/**
* Method for getting all saved video times
*
* @returns {Object}
*/
function get_all_video_times() {
return helpers.storage.get(save_player_pos_key) || {};
}
/**
* Method for removing all saved video times
*/
function remove_all_video_times() {
helpers.storage.remove(save_player_pos_key);
}
/**
* Method for setting the video time to a certain percentage
*
* @param {number} percent
*/
function set_time_percent(percent) {
const duration = player.duration();
const newTime = duration * (percent / 100);
player.currentTime(newTime);
}
/**
* Method for playing the video
*/
function play() { player.play(); }
/**
* Method for pausing the video
*/
function pause() { player.pause(); }
/**
* Method for stopping the video
*/
function stop() { player.pause(); player.currentTime(0); }
/**
* Method for toggling play/pause
*/
function toggle_play() { player.paused() ? play() : pause(); }
/**
* Method for toggling captions
*/
const toggle_captions = (function () {
let toggledTrack = null;
@ -585,10 +757,18 @@ const toggle_captions = (function () {
};
})();
/**
* Method for toggling fullscreen
*/
function toggle_fullscreen() {
player.isFullscreen() ? player.exitFullscreen() : player.requestFullscreen();
}
/**
* Method for increasing playback rate
*
* @param {number} steps
*/
function increase_playback_rate(steps) {
const maxIndex = options.playbackRates.length - 1;
const curIndex = options.playbackRates.indexOf(player.playbackRate());
@ -597,6 +777,7 @@ function increase_playback_rate(steps) {
player.playbackRate(options.playbackRates[newIndex]);
}
// Add event listener for keydown events
addEventListener('keydown', function (e) {
if (e.target.tagName.toLowerCase() === 'input') {
// Ignore input when focus is on certain elements, e.g. form fields.
@ -685,7 +866,7 @@ addEventListener('keydown', function (e) {
break;
// TODO: More precise step. Now FPS is taken equal to 29.97
// Common FPS: https://forum.videohelp.com/threads/81868#post323588
// Common FPS: https://forum.videohelp.com/threads/81864#post323588
// Possible solution is new HTMLVideoElement.requestVideoFrameCallback() https://wicg.github.io/video-rvfc/
case ',': action = function () { pause(); skip_seconds(-1/29.97); }; break;
case '.': action = function () { pause(); skip_seconds( 1/29.97); }; break;
@ -731,9 +912,6 @@ addEventListener('keydown', function (e) {
player.on('DOMMouseScroll', mouseScroll);
}());
// Since videojs-share can sometimes be blocked, we defer it until last
if (player.share) player.share(shareOptions);
// show the preferred caption by default
if (player_data.preferred_caption_found) {
player.ready(function () {
@ -748,6 +926,7 @@ if (player_data.preferred_caption_found) {
// Safari audio double duration fix
if (navigator.vendor === 'Apple Computer, Inc.' && video_data.params.listen) {
console.log('[Invidious Debug] Applying Safari listen mode duration fix...');
player.on('loadedmetadata', function () {
player.on('timeupdate', function () {
if (player.remainingTime() < player.duration() / 2 && player.remainingTime() >= 2) {
@ -759,6 +938,7 @@ if (navigator.vendor === 'Apple Computer, Inc.' && video_data.params.listen) {
// Safari screen timeout on looped video playback fix
if (navigator.vendor === 'Apple Computer, Inc.' && !video_data.params.listen && video_data.params.video_loop) {
console.log('[Invidious Debug] Applying Safari loop mode screen timeout fix...');
player.loop(false);
player.ready(function () {
player.on('ended', function () {
@ -770,19 +950,25 @@ if (navigator.vendor === 'Apple Computer, Inc.' && !video_data.params.listen &&
// Watch on Invidious link
if (location.pathname.startsWith('/embed/')) {
const Button = videojs.getComponent('Button');
let watch_on_invidious_button = new Button(player);
console.log('[Invidious Debug] Adding Watch on Invidious button...');
try {
const Button = videojs.getComponent('Button');
let watch_on_invidious_button = new Button(player);
// Create hyperlink for current instance
var redirect_element = document.createElement('a');
redirect_element.setAttribute('href', location.pathname.replace('/embed/', '/watch?v='));
redirect_element.appendChild(document.createTextNode('Invidious'));
// Create hyperlink for current instance
var redirect_element = document.createElement('a');
redirect_element.setAttribute('href', location.pathname.replace('/embed/', '/watch?v='));
redirect_element.appendChild(document.createTextNode('Invidious'));
watch_on_invidious_button.el().appendChild(redirect_element);
watch_on_invidious_button.addClass('watch-on-invidious');
watch_on_invidious_button.el().appendChild(redirect_element);
watch_on_invidious_button.addClass('watch-on-invidious');
var cb = player.getChild('ControlBar');
cb.addChild(watch_on_invidious_button);
var cb = player.getChild('ControlBar');
cb.addChild(watch_on_invidious_button);
console.log('[Invidious Debug] Watch on Invidious button added.');
} catch (e) {
console.error('[Invidious Debug] Watch on Invidious button FAILED:', e);
}
}
addEventListener('DOMContentLoaded', function () {
@ -792,3 +978,5 @@ addEventListener('DOMContentLoaded', function () {
changeInstanceLink.href = addCurrentTimeToURL(changeInstanceLink.href);
});
});
console.log('[Invidious Debug] player.js finished loading.');

View File

@ -90,14 +90,14 @@ db:
##
## API key for Invidious companion, used for securing the communication
## between Invidious and Invidious companion.
## The size of the key needs to be more or equal to 16.
## The key needs to be exactly 16 characters long.
##
## Note: This parameter is mandatory when Invidious companion is enabled
## and should be a random string.
## Such random string can be generated on linux with the following
## command: `pwgen 16 1`
##
## Accepted values: a string
## Accepted values: a string (of length 16)
## Default: <none>
##
#invidious_companion_key: "CHANGE_ME!!"

View File

@ -23,10 +23,16 @@ module Invidious::Frontend::WatchPage
return "<p id=\"download\">#{translate(locale, "Download is disabled")}</p>"
end
url = "/download"
if (CONFIG.invidious_companion.present?)
invidious_companion = CONFIG.invidious_companion.sample
url = "#{invidious_companion.public_url}/download?check=#{invidious_companion_encrypt(video.id)}"
end
return String.build(4000) do |str|
str << "<form"
str << " class=\"pure-form pure-form-stacked\""
str << " action='/download'"
str << " action='#{url}'"
str << " method='post'"
str << " rel='noopener'"
str << " target='_blank'>"

View File

@ -21,7 +21,7 @@ module Invidious::Routes::VideoPlayback
end
# Sanity check, to avoid being used as an open proxy
if !host.matches?(/[\w-]+.googlevideo.com/)
if !host.matches?(/[\w-]+\.(?:googlevideo|c\.youtube)\.com/)
return error_template(400, "Invalid \"host\" parameter.")
end
@ -37,7 +37,8 @@ module Invidious::Routes::VideoPlayback
# See: https://github.com/iv-org/invidious/issues/3302
range_header = env.request.headers["Range"]?
if range_header.nil?
sq = query_params["sq"]?
if range_header.nil? && sq.nil?
range_for_head = query_params["range"]? || "0-640"
headers["Range"] = "bytes=#{range_for_head}"
end

View File

@ -293,6 +293,9 @@ module Invidious::Routes::Watch
if CONFIG.disabled?("downloads")
return error_template(403, "Administrator has disabled this endpoint.")
end
if CONFIG.invidious_companion.present?
return error_template(403, "Downloads should be routed through Companion when present")
end
title = env.params.body["title"]? || ""
video_id = env.params.body["id"]? || ""
@ -328,13 +331,7 @@ module Invidious::Routes::Watch
env.params.query["title"] = filename
env.params.query["local"] = "true"
if (CONFIG.invidious_companion.present?)
video = get_video(video_id)
invidious_companion = CONFIG.invidious_companion.sample
return env.redirect "#{invidious_companion.public_url}/latest_version?#{env.params.query}"
else
return Invidious::Routes::VideoPlayback.latest_version(env)
end
return Invidious::Routes::VideoPlayback.latest_version(env)
else
return error_template(400, "Invalid label or itag")
end

View File

@ -109,27 +109,20 @@ def extract_video_info(video_id : String)
params["reason"] = JSON::Any.new(reason) if reason
if !CONFIG.invidious_companion.present?
new_player_response = nil
# Don't use Android test suite client if po_token is passed because po_token doesn't
# work for Android test suite client.
if reason.nil? && CONFIG.po_token.nil?
# Fetch the video streams using an Android client in order to get the
# decrypted URLs and maybe fix throttling issues (#2194). See the
# following issue for an explanation about decrypted URLs:
# https://github.com/TeamNewPipe/NewPipeExtractor/issues/562
client_config.client_type = YoutubeAPI::ClientType::AndroidTestSuite
new_player_response = try_fetch_streaming_data(video_id, client_config)
end
# Replace player response and reset reason
if !new_player_response.nil?
# Preserve captions & storyboard data before replacement
new_player_response["storyboards"] = player_response["storyboards"] if player_response["storyboards"]?
new_player_response["captions"] = player_response["captions"] if player_response["captions"]?
player_response = new_player_response
params.delete("reason")
if player_response["streamingData"]? && player_response.dig?("streamingData", "adaptiveFormats", 0, "url").nil?
LOGGER.warn("Missing URLs for adaptive formats, falling back to other YT clients.")
players_fallback = [YoutubeAPI::ClientType::WebMobile, YoutubeAPI::ClientType::TvHtml5]
players_fallback.each do |player_fallback|
client_config.client_type = player_fallback
player_fallback_response = try_fetch_streaming_data(video_id, client_config)
if player_fallback_response && player_fallback_response["streamingData"]? &&
player_fallback_response.dig?("streamingData", "adaptiveFormats", 0, "url")
streaming_data = player_response["streamingData"].as_h
streaming_data["adaptiveFormats"] = player_fallback_response["streamingData"]["adaptiveFormats"]
player_response["streamingData"] = JSON::Any.new(streaming_data)
break
end
end
end
end