mirror of
https://codeberg.org/SafeTwitch/safetwitch.git
synced 2024-12-22 13:22:58 -05:00
Audio-only support and invidious style selector 🥳 #25
This commit is contained in:
parent
a754961a14
commit
3e6cd185ae
3 changed files with 85 additions and 4 deletions
61
src/components/AudioPlayer.vue
Normal file
61
src/components/AudioPlayer.vue
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<audio ref="videoPlayer" class="w-full" controls></audio>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
// Importing video-js
|
||||||
|
import Hls from 'hls.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'VideoJsPlayer',
|
||||||
|
props: {
|
||||||
|
masterManifestUrl: {
|
||||||
|
type: String,
|
||||||
|
default() {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['PlayerTimeUpdate'],
|
||||||
|
async setup(props) {
|
||||||
|
let player: any
|
||||||
|
|
||||||
|
const getAudioOnlyManifestFromUrl = async (masterManifestUrl: string) => {
|
||||||
|
const manifestRes = await fetch(masterManifestUrl)
|
||||||
|
if (!manifestRes.ok) return;
|
||||||
|
|
||||||
|
const manifest = await manifestRes.text()
|
||||||
|
// The last line of the manifest is the
|
||||||
|
// audio only manifest. This is a bit hacky
|
||||||
|
// but it'll work. If issues arise we can
|
||||||
|
// always implement an actual m3u8 parser
|
||||||
|
const tmp = manifest.split("\n")
|
||||||
|
const audioOnlyManifest = tmp[tmp.length - 1]
|
||||||
|
|
||||||
|
return audioOnlyManifest
|
||||||
|
}
|
||||||
|
|
||||||
|
const audioOnlyManifest = await getAudioOnlyManifestFromUrl(props.masterManifestUrl)
|
||||||
|
return {
|
||||||
|
player,
|
||||||
|
audioOnlyManifest
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// initializing the video player
|
||||||
|
// when the component is being mounted
|
||||||
|
mounted() {
|
||||||
|
const video = this.$refs.videoPlayer as HTMLVideoElement;
|
||||||
|
if (Hls.isSupported()) {
|
||||||
|
const hls = new Hls();
|
||||||
|
|
||||||
|
hls.loadSource(this.audioOnlyManifest || "");
|
||||||
|
hls.attachMedia(video);
|
||||||
|
hls.on(Hls.Events.MANIFEST_PARSED, () => {
|
||||||
|
video.play();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
|
@ -22,6 +22,8 @@ app.provide('wsLink', `${wsProtocol}${import.meta.env.SAFETWITCH_BACKEND_DOMAIN}
|
||||||
import { OhVueIcon, addIcons } from 'oh-vue-icons'
|
import { OhVueIcon, addIcons } from 'oh-vue-icons'
|
||||||
import {
|
import {
|
||||||
IoSearchOutline,
|
IoSearchOutline,
|
||||||
|
BiHeadphones,
|
||||||
|
BiCameraVideoFill,
|
||||||
IoLink,
|
IoLink,
|
||||||
FaCircleNotch,
|
FaCircleNotch,
|
||||||
BiTwitter,
|
BiTwitter,
|
||||||
|
@ -35,6 +37,8 @@ import {
|
||||||
|
|
||||||
addIcons(
|
addIcons(
|
||||||
IoSearchOutline,
|
IoSearchOutline,
|
||||||
|
BiHeadphones,
|
||||||
|
BiCameraVideoFill,
|
||||||
IoLink,
|
IoLink,
|
||||||
FaCircleNotch,
|
FaCircleNotch,
|
||||||
BiTwitter,
|
BiTwitter,
|
||||||
|
|
|
@ -8,6 +8,7 @@ import ErrorMessage from '@/components/ErrorMessage.vue'
|
||||||
import FollowButton from '@/components/FollowButton.vue'
|
import FollowButton from '@/components/FollowButton.vue'
|
||||||
import LoadingScreen from '@/components/LoadingScreen.vue'
|
import LoadingScreen from '@/components/LoadingScreen.vue'
|
||||||
import VideoTab from '@/components/user/VideoTab.vue'
|
import VideoTab from '@/components/user/VideoTab.vue'
|
||||||
|
import AudioPlayer from '@/components/AudioPlayer.vue'
|
||||||
|
|
||||||
import type { StreamerData } from '@/types'
|
import type { StreamerData } from '@/types'
|
||||||
import { truncate, abbreviate, getEndpoint } from '@/mixins'
|
import { truncate, abbreviate, getEndpoint } from '@/mixins'
|
||||||
|
@ -32,11 +33,13 @@ export default {
|
||||||
],
|
],
|
||||||
fluid: true
|
fluid: true
|
||||||
}
|
}
|
||||||
|
const audioOptions = `${rootBackendUrl}/proxy/stream/${username}/hls.m3u8`
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data,
|
data,
|
||||||
status,
|
status,
|
||||||
videoOptions
|
videoOptions,
|
||||||
|
audioOptions
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
|
@ -56,7 +59,8 @@ export default {
|
||||||
ErrorMessage,
|
ErrorMessage,
|
||||||
FollowButton,
|
FollowButton,
|
||||||
LoadingScreen,
|
LoadingScreen,
|
||||||
VideoTab
|
VideoTab,
|
||||||
|
AudioPlayer
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
truncate,
|
truncate,
|
||||||
|
@ -78,7 +82,8 @@ export default {
|
||||||
class="flex bg-ctp-crust flex-col p-6 rounded-lg w-[99vw] md:max-w-prose md:min-w-[65ch] lg:max-w-[70rem] text-white"
|
class="flex bg-ctp-crust flex-col p-6 rounded-lg w-[99vw] md:max-w-prose md:min-w-[65ch] lg:max-w-[70rem] text-white"
|
||||||
>
|
>
|
||||||
<div v-if="data.isLive" class="w-full mx-auto rounded-lg mb-5">
|
<div v-if="data.isLive" class="w-full mx-auto rounded-lg mb-5">
|
||||||
<video-player :options="videoOptions"> </video-player>
|
<video-player v-if="Boolean($route.query['audio-only']) === false" :options="videoOptions"> </video-player>
|
||||||
|
<audio-player v-else :masterManifestUrl="audioOptions"></audio-player>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<img
|
<img
|
||||||
|
@ -105,7 +110,18 @@ export default {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ml-3 content-between">
|
<div class="ml-3 content-between">
|
||||||
<h1 class="text-2xl md:text-4xl font-bold">{{ data.username }}</h1>
|
<div>
|
||||||
|
<h1 class="text-2xl md:text-4xl font-bold inline-block">{{ data.username }}</h1>
|
||||||
|
<router-link v-if="$route.query['audio-only'] !== 'true'" to="?audio-only=true">
|
||||||
|
<v-icon name="bi-headphones" class="ml-1 w-8 h-8 inline-block"></v-icon>
|
||||||
|
</router-link>
|
||||||
|
<!-- For some reason it doesn't like going from
|
||||||
|
audio only to video, so we'll have the page reload
|
||||||
|
-->
|
||||||
|
<a v-else :href="$route.params.username.toString()">
|
||||||
|
<v-icon name="bi-camera-video-fill" class="ml-1 w-8 h-8 inline-block"></v-icon>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
<div v-if="data.stream" class="w-[14rem] md:w-[17rem]">
|
<div v-if="data.stream" class="w-[14rem] md:w-[17rem]">
|
||||||
<p class="text-sm font-bold text-gray-200 self-end">
|
<p class="text-sm font-bold text-gray-200 self-end">
|
||||||
{{ truncate(data.stream.title, 130) }}
|
{{ truncate(data.stream.title, 130) }}
|
||||||
|
|
Loading…
Reference in a new issue