mirror of
https://codeberg.org/SafeTwitch/safetwitch.git
synced 2024-12-22 05:12:57 -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 {
|
||||
IoSearchOutline,
|
||||
BiHeadphones,
|
||||
BiCameraVideoFill,
|
||||
IoLink,
|
||||
FaCircleNotch,
|
||||
BiTwitter,
|
||||
|
@ -35,6 +37,8 @@ import {
|
|||
|
||||
addIcons(
|
||||
IoSearchOutline,
|
||||
BiHeadphones,
|
||||
BiCameraVideoFill,
|
||||
IoLink,
|
||||
FaCircleNotch,
|
||||
BiTwitter,
|
||||
|
|
|
@ -8,6 +8,7 @@ import ErrorMessage from '@/components/ErrorMessage.vue'
|
|||
import FollowButton from '@/components/FollowButton.vue'
|
||||
import LoadingScreen from '@/components/LoadingScreen.vue'
|
||||
import VideoTab from '@/components/user/VideoTab.vue'
|
||||
import AudioPlayer from '@/components/AudioPlayer.vue'
|
||||
|
||||
import type { StreamerData } from '@/types'
|
||||
import { truncate, abbreviate, getEndpoint } from '@/mixins'
|
||||
|
@ -32,11 +33,13 @@ export default {
|
|||
],
|
||||
fluid: true
|
||||
}
|
||||
const audioOptions = `${rootBackendUrl}/proxy/stream/${username}/hls.m3u8`
|
||||
|
||||
return {
|
||||
data,
|
||||
status,
|
||||
videoOptions
|
||||
videoOptions,
|
||||
audioOptions
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
|
@ -56,7 +59,8 @@ export default {
|
|||
ErrorMessage,
|
||||
FollowButton,
|
||||
LoadingScreen,
|
||||
VideoTab
|
||||
VideoTab,
|
||||
AudioPlayer
|
||||
},
|
||||
methods: {
|
||||
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"
|
||||
>
|
||||
<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>
|
||||
|
||||
<img
|
||||
|
@ -105,7 +110,18 @@ export default {
|
|||
</div>
|
||||
|
||||
<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]">
|
||||
<p class="text-sm font-bold text-gray-200 self-end">
|
||||
{{ truncate(data.stream.title, 130) }}
|
||||
|
|
Loading…
Reference in a new issue