Added audio only mode
This commit is contained in:
@@ -17,6 +17,7 @@ Modern material design for Invidious
|
||||
- Live stream support.
|
||||
- Dash support (WIP).
|
||||
- Chapters.
|
||||
- Audio only mode.
|
||||
|
||||
## Todo
|
||||
- Port to [vidstack](https://www.vidstack.io/) once dash is supported.
|
||||
@@ -25,7 +26,6 @@ Modern material design for Invidious
|
||||
- Playlist support.
|
||||
- History deletion.
|
||||
- Load more for comments, history, subscriptions & search results.
|
||||
- Audio only mode.
|
||||
- PWA support.
|
||||
|
||||
## Previews
|
||||
|
||||
@@ -48,10 +48,11 @@ export interface AdaptiveFormats {
|
||||
clen: string;
|
||||
lmt: string;
|
||||
projectionType: number;
|
||||
container: string;
|
||||
container?: string;
|
||||
encoding: string;
|
||||
qualityLabel?: string;
|
||||
resolution?: string;
|
||||
audioQuality?: string;
|
||||
}
|
||||
|
||||
export interface FormatStreams {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import Hls from 'hls.js';
|
||||
import 'plyr/dist/plyr.css';
|
||||
import { SponsorBlock, type Category } from 'sponsorblock-api';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { onDestroy, onMount, tick } from 'svelte';
|
||||
import { get } from 'svelte/store';
|
||||
import { sponsorBlock as sponsorBlockStore, sponsorBlockUrl } from '../store';
|
||||
|
||||
@@ -24,6 +24,9 @@
|
||||
|
||||
export let data: { video: VideoPlay };
|
||||
export let currentTime: number = 0;
|
||||
export let audioMode = false;
|
||||
|
||||
let previousMode = structuredClone(audioMode);
|
||||
|
||||
export function seekTo(time: number) {
|
||||
if (typeof player !== 'undefined') {
|
||||
@@ -38,28 +41,95 @@
|
||||
let sponsorBlock: SponsorBlock | undefined;
|
||||
let hls: Hls | undefined;
|
||||
let dash: MediaPlayerClass | undefined;
|
||||
onMount(async () => {
|
||||
|
||||
$: {
|
||||
if (previousMode !== audioMode) {
|
||||
tick().then(async () => {
|
||||
document.getElementsByClassName('plyr')[0]?.remove();
|
||||
await createPlayer();
|
||||
previousMode = structuredClone(audioMode);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function createPlayer() {
|
||||
if (data.video.isUpcoming || !data.video.isListed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const playerPos = localStorage.getItem(data.video.videoId);
|
||||
|
||||
const tracks: Track[] = [];
|
||||
|
||||
for (const caption of data.video.captions) {
|
||||
// Have to preload captions, due to cors issue with how plyr
|
||||
// grabs captions
|
||||
if (!caption) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const captions = await fetch(`${get(invidiousInstance)}${caption.url}`);
|
||||
if (captions.status === 200) {
|
||||
tracks.push({
|
||||
kind: 'captions',
|
||||
label: caption.label,
|
||||
srcLang: caption.languageCode,
|
||||
src: URL.createObjectURL(await captions.blob())
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const sourceInfo: SourceInfo = {
|
||||
type: !audioMode ? 'video' : 'audio',
|
||||
previewThumbnails: {
|
||||
src: data.video.videoThumbnails[0].url
|
||||
},
|
||||
poster: data.video.videoThumbnails[0].url,
|
||||
tracks: tracks,
|
||||
sources: []
|
||||
};
|
||||
|
||||
const videoElement = document.getElementById('player') as HTMLMediaElement;
|
||||
|
||||
const controls = [
|
||||
'play-large',
|
||||
'play',
|
||||
'progress',
|
||||
'current-time',
|
||||
'duration',
|
||||
'volume',
|
||||
'captions',
|
||||
'settings',
|
||||
'download',
|
||||
'fullscreen'
|
||||
];
|
||||
|
||||
const qualitySettings = {
|
||||
default: 720,
|
||||
options: [4320, 2880, 2160, 1440, 1080, 720, 576, 480, 360, 240]
|
||||
};
|
||||
|
||||
if (audioMode) {
|
||||
qualitySettings.options = [];
|
||||
|
||||
data.video.adaptiveFormats.forEach((source) => {
|
||||
if (typeof source.audioQuality !== 'undefined') {
|
||||
const bitrate = Number(source.bitrate);
|
||||
qualitySettings.options.push(bitrate);
|
||||
sourceInfo.sources.push({
|
||||
src: source.url,
|
||||
type: source.type,
|
||||
size: bitrate
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
qualitySettings.default = qualitySettings.options[0];
|
||||
}
|
||||
|
||||
player = new Plyr(videoElement, {
|
||||
controls: [
|
||||
'play-large',
|
||||
'play',
|
||||
'progress',
|
||||
'current-time',
|
||||
'duration',
|
||||
'volume',
|
||||
'captions',
|
||||
'settings',
|
||||
'download',
|
||||
'fullscreen'
|
||||
]
|
||||
controls: controls,
|
||||
quality: qualitySettings
|
||||
});
|
||||
|
||||
if (get(sponsorBlockStore)) {
|
||||
@@ -103,67 +173,39 @@
|
||||
player.autoplay = get(playerAutoPlay);
|
||||
player.loop = get(playerAlwaysLoop);
|
||||
|
||||
const tracks: Track[] = [];
|
||||
if (!audioMode) {
|
||||
if (!data.video.hlsUrl) {
|
||||
if (get(playerDash)) {
|
||||
dash = Dash.MediaPlayer().create();
|
||||
dash.initialize(videoElement, data.video.dashUrl + '?local=true', get(playerAutoPlay));
|
||||
} else {
|
||||
const proxyVideos = get(playerProxyVideos);
|
||||
|
||||
for (const caption of data.video.captions) {
|
||||
// Have to preload captions, due to cors issue with how plyr
|
||||
// grabs captions
|
||||
if (!caption) {
|
||||
continue;
|
||||
}
|
||||
let src;
|
||||
sourceInfo.sources = data.video.formatStreams.map((format) => {
|
||||
if (proxyVideos) {
|
||||
const rawSrc = new URL(format.url);
|
||||
rawSrc.host = import.meta.env.VITE_DEFAULT_INVIDIOUS_INSTANCE.replace(
|
||||
'http://',
|
||||
''
|
||||
).replace('https://', '');
|
||||
|
||||
const captions = await fetch(`${get(invidiousInstance)}${caption.url}`);
|
||||
if (captions.status === 200) {
|
||||
tracks.push({
|
||||
kind: 'captions',
|
||||
label: caption.label,
|
||||
srcLang: caption.languageCode,
|
||||
src: URL.createObjectURL(await captions.blob())
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const sourceInfo: SourceInfo = {
|
||||
type: 'video',
|
||||
previewThumbnails: {
|
||||
src: data.video.videoThumbnails[0].url
|
||||
},
|
||||
poster: data.video.videoThumbnails[0].url,
|
||||
tracks: tracks,
|
||||
sources: []
|
||||
};
|
||||
|
||||
if (!data.video.hlsUrl) {
|
||||
if (get(playerDash)) {
|
||||
dash = Dash.MediaPlayer().create();
|
||||
dash.initialize(videoElement, data.video.dashUrl + '?local=true', get(playerAutoPlay));
|
||||
src = rawSrc.toString();
|
||||
} else {
|
||||
src = format.url;
|
||||
}
|
||||
return {
|
||||
src: src,
|
||||
size: Number(format.size.split('x')[1]),
|
||||
type: format.type
|
||||
};
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const proxyVideos = get(playerProxyVideos);
|
||||
|
||||
let src;
|
||||
sourceInfo.sources = data.video.formatStreams.map((format) => {
|
||||
if (proxyVideos) {
|
||||
const rawSrc = new URL(format.url);
|
||||
rawSrc.host = import.meta.env.VITE_DEFAULT_INVIDIOUS_INSTANCE.replace(
|
||||
'http://',
|
||||
''
|
||||
).replace('https://', '');
|
||||
|
||||
src = rawSrc.toString();
|
||||
} else {
|
||||
src = format.url;
|
||||
}
|
||||
return {
|
||||
src: src,
|
||||
size: Number(format.size.split('x')[1]),
|
||||
type: format.type
|
||||
};
|
||||
});
|
||||
hls = new Hls();
|
||||
hls.loadSource(data.video.hlsUrl + '?local=true');
|
||||
hls.attachMedia(videoElement);
|
||||
}
|
||||
} else {
|
||||
hls = new Hls();
|
||||
hls.loadSource(data.video.hlsUrl + '?local=true');
|
||||
hls.attachMedia(videoElement);
|
||||
}
|
||||
|
||||
player.source = sourceInfo;
|
||||
@@ -179,6 +221,10 @@
|
||||
'--plyr-menu-color',
|
||||
currentTheme['--secondary-text']
|
||||
);
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
await createPlayer();
|
||||
});
|
||||
|
||||
onDestroy(async () => {
|
||||
@@ -210,5 +256,11 @@
|
||||
{:else if !data.video.isListed}
|
||||
<h3>Video isn't listed</h3>
|
||||
{:else}
|
||||
<video id="player" playsinline controls> </video>
|
||||
{#if audioMode}
|
||||
<div style="margin-top: 50vh;"></div>
|
||||
{/if}
|
||||
|
||||
{#key audioMode}
|
||||
<video id="player" playsinline controls> </video>
|
||||
{/key}
|
||||
{/if}
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
import Player from '$lib/Player.svelte';
|
||||
import Thumbnail from '$lib/Thumbnail.svelte';
|
||||
import { cleanNumber, numberWithCommas } from '$lib/misc.js';
|
||||
import { activePage } from '../../../store.js';
|
||||
import { get } from 'svelte/store';
|
||||
import { activePage, playerListenByDefault } from '../../../store.js';
|
||||
|
||||
export let data;
|
||||
|
||||
@@ -21,6 +22,7 @@
|
||||
data.subscribed = !data.subscribed;
|
||||
}
|
||||
|
||||
let audioMode = get(playerListenByDefault);
|
||||
let currentTime: number;
|
||||
let seekTo: (time: number) => void;
|
||||
</script>
|
||||
@@ -29,7 +31,7 @@
|
||||
<div class="grid">
|
||||
<div class="s12 m12 l10">
|
||||
{#key data.video.videoId}
|
||||
<Player {data} bind:seekTo bind:currentTime />
|
||||
<Player {data} {audioMode} bind:seekTo bind:currentTime />
|
||||
{/key}
|
||||
|
||||
<h5>{data.video.title}</h5>
|
||||
@@ -72,6 +74,10 @@
|
||||
</button>
|
||||
</nav>
|
||||
{/if}
|
||||
<button on:click={() => (audioMode = !audioMode)} class:border={!audioMode}>
|
||||
<i>headphones</i>
|
||||
<span>Audio only </span>
|
||||
</button>
|
||||
<button class="border m l" data-ui="#share"
|
||||
><i>share</i> Share
|
||||
<menu class="left no-wrap" id="share" data-ui="#share">
|
||||
|
||||
@@ -16,9 +16,16 @@ export async function load({ params }) {
|
||||
comments = null;
|
||||
}
|
||||
|
||||
let returnYTDislikes;
|
||||
try {
|
||||
returnYTDislikes = get(returnYtDislikes) ? await getDislikes(params.slug) : null;
|
||||
} catch {
|
||||
returnYTDislikes = null;
|
||||
}
|
||||
|
||||
return {
|
||||
video: video,
|
||||
returnYTDislikes: get(returnYtDislikes) ? await getDislikes(params.slug) : null,
|
||||
returnYTDislikes: returnYTDislikes,
|
||||
comments: comments,
|
||||
subscribed: await amSubscribed(video.authorId),
|
||||
content: phaseDescription(video.description)
|
||||
|
||||
@@ -8,4 +8,4 @@ video {
|
||||
|
||||
.plyr:fullscreen video {
|
||||
max-height: initial !important;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user