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 13s
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 - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with: # - name: Set up QEMU
platforms: arm64 # 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 - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
@ -34,20 +41,18 @@ jobs:
- name: Login to registry - name: Login to registry
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: quay.io registry: gitea.ghost.tel
username: ${{ secrets.QUAY_USERNAME }} username: ${{ secrets.USERNAME }}
password: ${{ secrets.QUAY_PASSWORD }} password: ${{ secrets.PASSWORD }}
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v5
with: with:
images: quay.io/invidious/invidious images: gitea.ghost.tel/knight/invidious
tags: | tags: |
type=sha,format=short,prefix={{date 'YYYY.MM.DD'}}-,enable=${{ github.ref == format('refs/heads/{0}', 'master') }} 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') }} 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 - name: Build and push Docker AMD64 image for Push Event
uses: docker/build-push-action@v5 uses: docker/build-push-action@v5
@ -55,7 +60,6 @@ jobs:
context: . context: .
file: docker/Dockerfile file: docker/Dockerfile
platforms: linux/amd64 platforms: linux/amd64
labels: ${{ steps.meta.outputs.labels }}
push: true push: true
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
build-args: | build-args: |
@ -65,23 +69,9 @@ jobs:
id: meta-arm64 id: meta-arm64
uses: docker/metadata-action@v5 uses: docker/metadata-action@v5
with: with:
images: quay.io/invidious/invidious images: gitea.ghost.tel/knight/invidious
flavor: | flavor: |
suffix=-arm64 suffix=-arm64
tags: | tags: |
type=sha,format=short,prefix={{date 'YYYY.MM.DD'}}-,enable=${{ github.ref == format('refs/heads/{0}', 'master') }} 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') }} 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) ## 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 ## v2.20250314.0
### Wrap-up ### Wrap-up

View File

@ -81,9 +81,9 @@
- [Available in many languages](locales/), thanks to [our translators](#contribute) - [Available in many languages](locales/), thanks to [our translators](#contribute)
**Data import/export** **Data import/export**
- Import subscriptions from YouTube, NewPipe and Freetube - Import subscriptions from YouTube, NewPipe and FreeTube
- Import watch history from YouTube and NewPipe - Import watch history from YouTube and NewPipe
- Export subscriptions to NewPipe and Freetube - Export subscriptions to NewPipe and FreeTube
- Import/Export Invidious user data - Import/Export Invidious user data
**Technical features** **Technical features**
@ -95,11 +95,11 @@
## Quick start ## Quick start
**Using invidious:** **Using Invidious:**
- [Select a public instance from the list](https://instances.invidious.io) and start watching videos right now! - [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/) - [Follow the installation instructions](https://docs.invidious.io/installation/)
@ -114,8 +114,8 @@ https://github.com/iv-org/documentation
### Extensions ### Extensions
We highly recommend the use of [Privacy Redirect](https://github.com/SimonBrazell/privacy-redirect#get), 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 a browser extension that automatically redirects YouTube URLs to any Invidious instance and replaces
embedded youtube videos on other websites with invidious. embedded YouTube videos on other websites with Invidious.
The documentation contains a list of browser extensions that we recommended to use along 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/. 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. 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 ## Projects using Invidious

View File

@ -1,6 +1,8 @@
'use strict'; 'use strict';
console.log('[Invidious Debug] player.js start');
var player_data = JSON.parse(document.getElementById('player_data').textContent); var player_data = JSON.parse(document.getElementById('player_data').textContent);
var video_data = JSON.parse(document.getElementById('video_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 = { var options = {
liveui: true, liveui: true,
@ -50,9 +52,27 @@ videojs.Vhs.xhr.beforeRequest = function(options) {
return 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 () { 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; if (video_data.params.quality === 'dash') return;
var localNotDisabled = ( var localNotDisabled = (
@ -70,6 +90,7 @@ player.on('error', function () {
return source; return source;
})); }));
} else if (reloadMakesSense) { } else if (reloadMakesSense) {
console.log('[Invidious Debug] Player error: Attempting reload in 5 seconds.');
setTimeout(function () { setTimeout(function () {
console.warn('An error occurred in the player, reloading...'); console.warn('An error occurred in the player, reloading...');
@ -86,13 +107,54 @@ player.on('error', function () {
player.playbackRate(playbackRate); player.playbackRate(playbackRate);
if (!paused) player.play(); if (!paused) player.play();
}, 5000); }, 5000);
} else {
console.log('[Invidious Debug] Player error: No specific action taken.');
} }
}); });
if (video_data.params.quality === 'dash') { try {
player.reloadSourceOnError({ player.on('error', function () {
errorInterval: 10 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 * 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) { if (video_data.params.save_player_pos) {
const url = new URL(location); console.log('[Invidious Debug] Setting up player position save/restore...');
const hasTimeParam = url.searchParams.has('t'); var lastUpdated = 0;
const rememberedTime = get_video_time(); var save_video_time = function(time) {
let lastUpdated = 0; const all_video_times = get_all_video_times();
all_video_times[video_data.id] = time;
if(!hasTimeParam) { helpers.storage.set(save_player_pos_key, all_video_times);
if (rememberedTime >= video_data.length_seconds - 20)
set_seconds_after_start(0);
else
set_seconds_after_start(rememberedTime);
} }
player.on('timeupdate', function () { player.on('timeupdate', function () {
@ -376,64 +324,138 @@ if (video_data.params.save_player_pos) {
else remove_all_video_times(); else remove_all_video_times();
if (video_data.params.autoplay) { if (video_data.params.autoplay) {
console.log('[Invidious Debug] Handling autoplay setup...');
var bpb = player.getChild('bigPlayButton'); var bpb = player.getChild('bigPlayButton');
bpb.hide(); bpb.hide();
player.ready(function () { player.ready(function () {
console.log('[Invidious Debug] Player ready for autoplay.');
new Promise(function (resolve, reject) { new Promise(function (resolve, reject) {
setTimeout(function () {resolve(1);}, 1); setTimeout(function () {resolve(1);}, 1);
}).then(function (result) { }).then(function (result) {
console.log('[Invidious Debug] Attempting autoplay...');
console.log('[Invidious Debug] Calling player.play() for autoplay...');
var promise = player.play(); var promise = player.play();
if (promise !== undefined) { if (promise !== undefined) {
promise.then(function () { promise.then(function () {
console.log('[Invidious Debug] Autoplay successful.');
}).catch(function (error) { }).catch(function (error) {
console.error('[Invidious Debug] Autoplay FAILED:', error);
bpb.show(); bpb.show();
}); });
} else {
console.log('[Invidious Debug] Autoplay started (no promise returned).');
} }
}); });
}); });
} }
if (!video_data.params.listen && video_data.params.quality === 'dash') { 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.ready(function () {
player.on('loadedmetadata', function () { // Add listener for quality level errors
const qualityLevels = Array.from(player.qualityLevels()).sort(function (a, b) {return a.height - b.height;}); const qualityLevels = player.qualityLevels();
let targetQualityLevel; if (qualityLevels) {
switch (video_data.params.quality_dash) { qualityLevels.on('error', function(event) {
case 'best': console.error('[Invidious Debug] QualityLevels Error Event:', event);
targetQualityLevel = qualityLevels.length - 1; });
break; console.log('[Invidious Debug] QualityLevels error listener attached.'); // Added
case 'worst':
targetQualityLevel = 0; // Log initial quality levels once metadata is loaded
break; player.on('loadedmetadata', function() {
default: try {
const targetHeight = parseInt(video_data.params.quality_dash); console.log('[Invidious Debug] Event: loadedmetadata triggered.'); // Added
for (let i = 0; i < qualityLevels.length; i++) { const levels = Array.from(player.qualityLevels());
if (qualityLevels[i].height <= targetHeight) // Log detailed info about each level
targetQualityLevel = i; 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}))));
else } 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; 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) {
qualityLevels.forEach(function (level, index) { level.enabled = (index === targetQualityLevel);
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({ console.log('[Invidious Debug] Initializing vttThumbnails...');
src: '/api/v1/storyboards/' + video_data.id + '?height=90', try {
showTimestamp: true 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 // Enable annotations
if (!video_data.params.listen && video_data.params.annotations) { if (!video_data.params.listen && video_data.params.annotations) {
console.log('[Invidious Debug] Initializing annotations...');
addEventListener('load', function (e) { addEventListener('load', function (e) {
addEventListener('__ar_annotation_click', function (e) { addEventListener('__ar_annotation_click', function (e) {
const url = e.detail.url, const url = e.detail.url,
@ -468,65 +490,215 @@ if (!video_data.params.listen && video_data.params.annotations) {
} else { } else {
player.youtubeAnnotationsPlugin({ annotationXml: response, videoContainer: video_container }); player.youtubeAnnotationsPlugin({ annotationXml: response, videoContainer: video_container });
} }
console.log('[Invidious Debug] Annotations initialized.');
} }
}); });
}); });
} }
function change_volume(delta) { player.on('play', function() {
const curVolume = player.volume(); console.log('[Invidious Debug] Event: play triggered. Current source:', JSON.stringify(player.currentSource()));
let newVolume = curVolume + delta; });
newVolume = helpers.clamp(newVolume, 0, 1);
player.volume(newVolume); 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() { // Detect mobile users and initialize mobileUi for better UX
player.muted(!player.muted()); // 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) { if (isMobile()) {
const duration = player.duration(); console.log('[Invidious Debug] Mobile detected, initializing mobile UI...');
const curTime = player.currentTime(); try {
let newTime = curTime + delta; player.mobileUi({ touchControls: { seekSeconds: 5, tapTimeout: 200, disableOnEnd: true } });
newTime = helpers.clamp(newTime, 0, duration); console.log('[Invidious Debug] Mobile UI initialized.');
player.currentTime(newTime); } catch (e) {
console.error('[Invidious Debug] Mobile UI FAILED:', e);
}
} }
function set_seconds_after_start(delta) { if (player.markers && !video_data.params.listen && video_data.params.chapters) {
const start = video_data.params.video_start; console.log('[Invidious Debug] Initializing markers...');
player.currentTime(start + delta); 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) { function save_video_time(seconds) {
const all_video_times = get_all_video_times(); const all_video_times = get_all_video_times();
all_video_times[video_data.id] = seconds; all_video_times[video_data.id] = seconds;
helpers.storage.set(save_player_pos_key, all_video_times); helpers.storage.set(save_player_pos_key, all_video_times);
} }
/**
* Method for getting the saved video time
*
* @returns {number}
*/
function get_video_time() { function get_video_time() {
return get_all_video_times()[video_data.id] || 0; return get_all_video_times()[video_data.id] || 0;
} }
/**
* Method for getting all saved video times
*
* @returns {Object}
*/
function get_all_video_times() { function get_all_video_times() {
return helpers.storage.get(save_player_pos_key) || {}; return helpers.storage.get(save_player_pos_key) || {};
} }
/**
* Method for removing all saved video times
*/
function remove_all_video_times() { function remove_all_video_times() {
helpers.storage.remove(save_player_pos_key); 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) { function set_time_percent(percent) {
const duration = player.duration(); const duration = player.duration();
const newTime = duration * (percent / 100); const newTime = duration * (percent / 100);
player.currentTime(newTime); player.currentTime(newTime);
} }
/**
* Method for playing the video
*/
function play() { player.play(); } function play() { player.play(); }
/**
* Method for pausing the video
*/
function pause() { player.pause(); } function pause() { player.pause(); }
/**
* Method for stopping the video
*/
function stop() { player.pause(); player.currentTime(0); } function stop() { player.pause(); player.currentTime(0); }
/**
* Method for toggling play/pause
*/
function toggle_play() { player.paused() ? play() : pause(); } function toggle_play() { player.paused() ? play() : pause(); }
/**
* Method for toggling captions
*/
const toggle_captions = (function () { const toggle_captions = (function () {
let toggledTrack = null; let toggledTrack = null;
@ -585,10 +757,18 @@ const toggle_captions = (function () {
}; };
})(); })();
/**
* Method for toggling fullscreen
*/
function toggle_fullscreen() { function toggle_fullscreen() {
player.isFullscreen() ? player.exitFullscreen() : player.requestFullscreen(); player.isFullscreen() ? player.exitFullscreen() : player.requestFullscreen();
} }
/**
* Method for increasing playback rate
*
* @param {number} steps
*/
function increase_playback_rate(steps) { function increase_playback_rate(steps) {
const maxIndex = options.playbackRates.length - 1; const maxIndex = options.playbackRates.length - 1;
const curIndex = options.playbackRates.indexOf(player.playbackRate()); const curIndex = options.playbackRates.indexOf(player.playbackRate());
@ -597,6 +777,7 @@ function increase_playback_rate(steps) {
player.playbackRate(options.playbackRates[newIndex]); player.playbackRate(options.playbackRates[newIndex]);
} }
// Add event listener for keydown events
addEventListener('keydown', function (e) { addEventListener('keydown', function (e) {
if (e.target.tagName.toLowerCase() === 'input') { if (e.target.tagName.toLowerCase() === 'input') {
// Ignore input when focus is on certain elements, e.g. form fields. // Ignore input when focus is on certain elements, e.g. form fields.
@ -685,7 +866,7 @@ addEventListener('keydown', function (e) {
break; break;
// TODO: More precise step. Now FPS is taken equal to 29.97 // 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/ // 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;
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); 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 // show the preferred caption by default
if (player_data.preferred_caption_found) { if (player_data.preferred_caption_found) {
player.ready(function () { player.ready(function () {
@ -748,6 +926,7 @@ if (player_data.preferred_caption_found) {
// Safari audio double duration fix // Safari audio double duration fix
if (navigator.vendor === 'Apple Computer, Inc.' && video_data.params.listen) { 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('loadedmetadata', function () {
player.on('timeupdate', function () { player.on('timeupdate', function () {
if (player.remainingTime() < player.duration() / 2 && player.remainingTime() >= 2) { 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 // Safari screen timeout on looped video playback fix
if (navigator.vendor === 'Apple Computer, Inc.' && !video_data.params.listen && video_data.params.video_loop) { 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.loop(false);
player.ready(function () { player.ready(function () {
player.on('ended', function () { player.on('ended', function () {
@ -770,19 +950,25 @@ if (navigator.vendor === 'Apple Computer, Inc.' && !video_data.params.listen &&
// Watch on Invidious link // Watch on Invidious link
if (location.pathname.startsWith('/embed/')) { if (location.pathname.startsWith('/embed/')) {
const Button = videojs.getComponent('Button'); console.log('[Invidious Debug] Adding Watch on Invidious button...');
let watch_on_invidious_button = new Button(player); try {
const Button = videojs.getComponent('Button');
let watch_on_invidious_button = new Button(player);
// Create hyperlink for current instance // Create hyperlink for current instance
var redirect_element = document.createElement('a'); var redirect_element = document.createElement('a');
redirect_element.setAttribute('href', location.pathname.replace('/embed/', '/watch?v=')); redirect_element.setAttribute('href', location.pathname.replace('/embed/', '/watch?v='));
redirect_element.appendChild(document.createTextNode('Invidious')); redirect_element.appendChild(document.createTextNode('Invidious'));
watch_on_invidious_button.el().appendChild(redirect_element); watch_on_invidious_button.el().appendChild(redirect_element);
watch_on_invidious_button.addClass('watch-on-invidious'); watch_on_invidious_button.addClass('watch-on-invidious');
var cb = player.getChild('ControlBar'); var cb = player.getChild('ControlBar');
cb.addChild(watch_on_invidious_button); 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 () { addEventListener('DOMContentLoaded', function () {
@ -792,3 +978,5 @@ addEventListener('DOMContentLoaded', function () {
changeInstanceLink.href = addCurrentTimeToURL(changeInstanceLink.href); 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 ## API key for Invidious companion, used for securing the communication
## between Invidious and Invidious companion. ## 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 ## Note: This parameter is mandatory when Invidious companion is enabled
## and should be a random string. ## and should be a random string.
## Such random string can be generated on linux with the following ## Such random string can be generated on linux with the following
## command: `pwgen 16 1` ## command: `pwgen 16 1`
## ##
## Accepted values: a string ## Accepted values: a string (of length 16)
## Default: <none> ## Default: <none>
## ##
#invidious_companion_key: "CHANGE_ME!!" #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>" return "<p id=\"download\">#{translate(locale, "Download is disabled")}</p>"
end 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| return String.build(4000) do |str|
str << "<form" str << "<form"
str << " class=\"pure-form pure-form-stacked\"" str << " class=\"pure-form pure-form-stacked\""
str << " action='/download'" str << " action='#{url}'"
str << " method='post'" str << " method='post'"
str << " rel='noopener'" str << " rel='noopener'"
str << " target='_blank'>" str << " target='_blank'>"

View File

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

View File

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

View File

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