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