Add stream notifications, clean up connected status.
This commit is contained in:
parent
901faaee50
commit
f161b6c806
|
@ -8,125 +8,94 @@
|
|||
</h5>
|
||||
</div>
|
||||
<div class="card-body pt-0">
|
||||
<div class="form-row pb-4">
|
||||
<div class="col-sm-12">
|
||||
<ul class="nav nav-tabs card-header-tabs mt-0">
|
||||
<li class="nav-item">
|
||||
<a
|
||||
class="nav-link active"
|
||||
href="#settings"
|
||||
data-toggle="tab"
|
||||
>
|
||||
{{ $gettext('Settings') }}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a
|
||||
class="nav-link"
|
||||
href="#metadata"
|
||||
data-toggle="tab"
|
||||
>
|
||||
{{ $gettext('Metadata') }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col-sm-12">
|
||||
<div class="tab-content mt-1">
|
||||
<div
|
||||
id="settings"
|
||||
class="tab-pane active"
|
||||
<template v-if="isConnected">
|
||||
<div class="form-group">
|
||||
<label
|
||||
for="metadata_title"
|
||||
class="mb-2"
|
||||
>
|
||||
{{ $gettext('Title') }}
|
||||
</label>
|
||||
<div class="controls">
|
||||
<input
|
||||
id="metadata_title"
|
||||
v-model="shownMetadata.title"
|
||||
class="form-control"
|
||||
type="text"
|
||||
>
|
||||
<div class="form-group">
|
||||
<label class="mb-2">
|
||||
{{ $gettext('DJ Credentials') }}
|
||||
</label>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="col-6">
|
||||
<input
|
||||
v-model="djUsername"
|
||||
type="text"
|
||||
class="form-control"
|
||||
:placeholder="$gettext('Username')"
|
||||
>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<input
|
||||
v-model="djPassword"
|
||||
type="password"
|
||||
class="form-control"
|
||||
:placeholder="$gettext('Password')"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
id="metadata"
|
||||
class="tab-pane"
|
||||
>
|
||||
<div class="form-group">
|
||||
<label
|
||||
for="metadata_title"
|
||||
class="mb-2"
|
||||
>
|
||||
{{ $gettext('Title') }}
|
||||
</label>
|
||||
<div class="controls">
|
||||
<input
|
||||
id="metadata_title"
|
||||
v-model="shownMetadata.title"
|
||||
class="form-control"
|
||||
type="text"
|
||||
:disabled="!isStreaming"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label
|
||||
for="metadata_artist"
|
||||
class="mb-2"
|
||||
>
|
||||
{{ $gettext('Artist') }}
|
||||
</label>
|
||||
<div class="controls">
|
||||
<input
|
||||
id="metadata_artist"
|
||||
v-model="shownMetadata.artist"
|
||||
class="form-control"
|
||||
type="text"
|
||||
:disabled="!isStreaming"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
:disabled="!isStreaming"
|
||||
@click="updateMetadata"
|
||||
>
|
||||
{{ $gettext('Update Metadata') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label
|
||||
for="metadata_artist"
|
||||
class="mb-2"
|
||||
>
|
||||
{{ $gettext('Artist') }}
|
||||
</label>
|
||||
<div class="controls">
|
||||
<input
|
||||
id="metadata_artist"
|
||||
v-model="shownMetadata.artist"
|
||||
class="form-control"
|
||||
type="text"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
@click="updateMetadata"
|
||||
>
|
||||
{{ $gettext('Update Metadata') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="form-group">
|
||||
<label
|
||||
for="dj_username"
|
||||
class="mb-2"
|
||||
>
|
||||
{{ $gettext('Username') }}
|
||||
</label>
|
||||
<div class="controls">
|
||||
<input
|
||||
id="dj_username"
|
||||
v-model="djUsername"
|
||||
type="text"
|
||||
class="form-control"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label
|
||||
for="dj_password"
|
||||
class="mb-2"
|
||||
>
|
||||
{{ $gettext('Password') }}
|
||||
</label>
|
||||
<div class="controls">
|
||||
<input
|
||||
id="dj_password"
|
||||
v-model="djPassword"
|
||||
type="password"
|
||||
class="form-control"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="card-actions">
|
||||
<button
|
||||
v-if="!isStreaming"
|
||||
v-if="!isConnected"
|
||||
class="btn btn-success"
|
||||
@click="startStream(djUsername, djPassword)"
|
||||
>
|
||||
{{ langStreamButton }}
|
||||
</button>
|
||||
<button
|
||||
v-if="isStreaming"
|
||||
v-if="isConnected"
|
||||
class="btn btn-danger"
|
||||
@click="stopStream"
|
||||
>
|
||||
|
@ -144,8 +113,7 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import {computed, inject, ref} from "vue";
|
||||
import {syncRef} from "@vueuse/core";
|
||||
import {computed, inject, ref, watch} from "vue";
|
||||
import {useTranslate} from "~/vendor/gettext";
|
||||
|
||||
const props = defineProps({
|
||||
|
@ -160,7 +128,7 @@ const djPassword = ref(null);
|
|||
|
||||
const {
|
||||
doPassThrough,
|
||||
isStreaming,
|
||||
isConnected,
|
||||
startStream,
|
||||
stopStream,
|
||||
metadata,
|
||||
|
@ -170,13 +138,22 @@ const {
|
|||
const {$gettext} = useTranslate();
|
||||
|
||||
const langStreamButton = computed(() => {
|
||||
return (isStreaming.value)
|
||||
return (isConnected.value)
|
||||
? $gettext('Stop Streaming')
|
||||
: $gettext('Start Streaming');
|
||||
});
|
||||
|
||||
const shownMetadata = ref({});
|
||||
syncRef(metadata, shownMetadata, {direction: "ltr"});
|
||||
watch(metadata, (newMeta) => {
|
||||
if (newMeta === null) {
|
||||
newMeta = {
|
||||
artist: '',
|
||||
title: ''
|
||||
};
|
||||
}
|
||||
|
||||
shownMetadata.value = newMeta;
|
||||
});
|
||||
|
||||
const updateMetadata = () => {
|
||||
sendMetadata(shownMetadata.value);
|
||||
|
|
|
@ -2,8 +2,9 @@ import {ref} from "vue";
|
|||
import {useUserMedia} from "@vueuse/core";
|
||||
|
||||
export function useWebDjNode(webcaster) {
|
||||
const {isConnected, connect: connectSocket, metadata, sendMetadata} = webcaster;
|
||||
|
||||
const doPassThrough = ref(false);
|
||||
const isStreaming = ref(false);
|
||||
|
||||
const context = new AudioContext({
|
||||
sampleRate: 44100
|
||||
|
@ -12,7 +13,7 @@ export function useWebDjNode(webcaster) {
|
|||
const sink = context.createScriptProcessor(256, 2, 2);
|
||||
|
||||
sink.onaudioprocess = (buf) => {
|
||||
for (let channel = 0; channel < buf.inputBuffer.numberOfChannels - 1; channel++) {
|
||||
for (let channel = 0; channel < buf.inputBuffer.numberOfChannels; channel++) {
|
||||
let channelData = buf.inputBuffer.getChannelData(channel);
|
||||
buf.outputBuffer.getChannelData(channel).set(channelData);
|
||||
}
|
||||
|
@ -21,7 +22,7 @@ export function useWebDjNode(webcaster) {
|
|||
const passThrough = context.createScriptProcessor(256, 2, 2);
|
||||
|
||||
passThrough.onaudioprocess = (buf) => {
|
||||
for (let channel = 0; channel < buf.inputBuffer.numberOfChannels - 1; channel++) {
|
||||
for (let channel = 0; channel < buf.inputBuffer.numberOfChannels; channel++) {
|
||||
let channelData = buf.inputBuffer.getChannelData(channel);
|
||||
|
||||
if (doPassThrough.value) {
|
||||
|
@ -43,8 +44,6 @@ export function useWebDjNode(webcaster) {
|
|||
let mediaRecorder;
|
||||
|
||||
const startStream = (username = null, password = null) => {
|
||||
isStreaming.value = true;
|
||||
|
||||
context.resume();
|
||||
|
||||
mediaRecorder = new MediaRecorder(
|
||||
|
@ -55,14 +54,13 @@ export function useWebDjNode(webcaster) {
|
|||
}
|
||||
);
|
||||
|
||||
webcaster.connect(mediaRecorder, username, password);
|
||||
connectSocket(mediaRecorder, username, password);
|
||||
|
||||
mediaRecorder.start(1000);
|
||||
}
|
||||
|
||||
const stopStream = () => {
|
||||
mediaRecorder?.stop();
|
||||
isStreaming.value = false;
|
||||
};
|
||||
|
||||
const createAudioSource = ({file, audio}, cb, onEnd) => {
|
||||
|
@ -124,16 +122,9 @@ export function useWebDjNode(webcaster) {
|
|||
return cb(stream);
|
||||
};
|
||||
|
||||
const metadata = ref({});
|
||||
|
||||
const sendMetadata = (data) => {
|
||||
webcaster.sendMetadata(data);
|
||||
metadata.value = data;
|
||||
};
|
||||
|
||||
return {
|
||||
doPassThrough,
|
||||
isStreaming,
|
||||
isConnected,
|
||||
context,
|
||||
sink,
|
||||
passThrough,
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
import {ref, shallowRef} from "vue";
|
||||
import {useNotify} from "~/vendor/bootstrapVue";
|
||||
import {useTranslate} from "~/vendor/gettext";
|
||||
|
||||
export const webcasterProps = {
|
||||
baseUri: {
|
||||
type: String,
|
||||
|
@ -8,16 +12,27 @@ export const webcasterProps = {
|
|||
export function useWebcaster(props) {
|
||||
const {baseUri} = props;
|
||||
|
||||
const {notifySuccess, notifyError} = useNotify();
|
||||
const {$gettext} = useTranslate();
|
||||
|
||||
const metadata = shallowRef(null);
|
||||
const isConnected = ref(false);
|
||||
|
||||
let socket = null;
|
||||
let mediaRecorder = null;
|
||||
|
||||
const isConnected = () => {
|
||||
return socket !== null && socket.readyState === WebSocket.OPEN;
|
||||
};
|
||||
const sendMetadata = (data) => {
|
||||
metadata.value = data;
|
||||
|
||||
const connect = (newMediaRecorder, username = null, password = null) => {
|
||||
if (isConnected.value) {
|
||||
socket.send(JSON.stringify({
|
||||
type: "metadata",
|
||||
data,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
const connect = (mediaRecorder, username = null, password = null) => {
|
||||
socket = new WebSocket(baseUri, "webcast");
|
||||
mediaRecorder = newMediaRecorder;
|
||||
|
||||
let hello = {
|
||||
mime: mediaRecorder.mimeType,
|
||||
|
@ -34,7 +49,27 @@ export function useWebcaster(props) {
|
|||
socket.send(JSON.stringify({
|
||||
type: "hello",
|
||||
data: hello
|
||||
}))
|
||||
}));
|
||||
|
||||
isConnected.value = true;
|
||||
|
||||
setTimeout(() => {
|
||||
if (isConnected.value) {
|
||||
notifySuccess($gettext('WebDJ connected!'));
|
||||
|
||||
if (metadata.value !== null) {
|
||||
sendMetadata(metadata.value);
|
||||
}
|
||||
}
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
socket.onerror = () => {
|
||||
notifyError($gettext('An error occurred with the WebDJ socket.'));
|
||||
}
|
||||
|
||||
socket.onclose = () => {
|
||||
isConnected.value = false;
|
||||
};
|
||||
|
||||
mediaRecorder.ondataavailable = async (e) => {
|
||||
|
@ -51,18 +86,10 @@ export function useWebcaster(props) {
|
|||
};
|
||||
};
|
||||
|
||||
const sendMetadata = (data) => {
|
||||
if (isConnected()) {
|
||||
socket.send(JSON.stringify({
|
||||
type: "metadata",
|
||||
data,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isConnected,
|
||||
connect,
|
||||
metadata,
|
||||
sendMetadata
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue