mirror of
https://github.com/AzuraCast/AzuraCast.git
synced 2024-06-14 05:06:37 +00:00
Add sample rate/bitrate back to WebDJ, optimize reactivity of WebDJ components.
This commit is contained in:
parent
62d68f5a7c
commit
5a292d4330
|
@ -43,7 +43,7 @@ import PlaylistPanel from './WebDJ/PlaylistPanel.vue';
|
|||
import SettingsPanel from './WebDJ/SettingsPanel.vue';
|
||||
import {useProvideWebDjNode, useWebDjNode} from "~/components/Public/WebDJ/useWebDjNode";
|
||||
import {ref} from "vue";
|
||||
import {useWebcaster, webcasterProps} from "~/components/Public/WebDJ/useWebcaster";
|
||||
import {useProvideWebcaster, useWebcaster, webcasterProps} from "~/components/Public/WebDJ/useWebcaster";
|
||||
import {useProvideMixer} from "~/components/Public/WebDJ/useMixerValue";
|
||||
import {useProvidePassthroughSync} from "~/components/Public/WebDJ/usePassthroughSync";
|
||||
|
||||
|
@ -56,6 +56,7 @@ const props = defineProps({
|
|||
});
|
||||
|
||||
const webcaster = useWebcaster(props);
|
||||
useProvideWebcaster(webcaster);
|
||||
|
||||
const node = useWebDjNode(webcaster);
|
||||
useProvideWebDjNode(node);
|
||||
|
|
|
@ -84,9 +84,9 @@ import {useDevicesList} from "@vueuse/core";
|
|||
import {ref, watch} from "vue";
|
||||
import {useWebDjTrack} from "~/components/Public/WebDJ/useWebDjTrack";
|
||||
import {usePassthroughSync} from "~/components/Public/WebDJ/usePassthroughSync";
|
||||
import {useWebDjSource} from "~/components/Public/WebDJ/useWebDjSource";
|
||||
|
||||
const {
|
||||
createMicrophoneSource,
|
||||
source,
|
||||
isPlaying,
|
||||
trackGain,
|
||||
|
@ -96,6 +96,10 @@ const {
|
|||
stop
|
||||
} = useWebDjTrack();
|
||||
|
||||
const {
|
||||
createMicrophoneSource
|
||||
} = useWebDjSource();
|
||||
|
||||
usePassthroughSync(trackPassThrough, 'microphone');
|
||||
|
||||
const {audioInputs} = useDevicesList({
|
||||
|
|
|
@ -177,6 +177,8 @@ import {useTranslate} from "~/vendor/gettext";
|
|||
import {forEach} from "lodash";
|
||||
import {useInjectMixer} from "~/components/Public/WebDJ/useMixerValue";
|
||||
import {usePassthroughSync} from "~/components/Public/WebDJ/usePassthroughSync";
|
||||
import {useWebDjSource} from "~/components/Public/WebDJ/useWebDjSource";
|
||||
import {useInjectWebcaster} from "~/components/Public/WebDJ/useWebcaster";
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
|
@ -190,8 +192,6 @@ const isLeftPlaylist = computed(() => {
|
|||
});
|
||||
|
||||
const {
|
||||
createAudioSource,
|
||||
sendMetadata,
|
||||
source,
|
||||
isPlaying,
|
||||
isPaused,
|
||||
|
@ -204,6 +204,14 @@ const {
|
|||
stop
|
||||
} = useWebDjTrack();
|
||||
|
||||
const {
|
||||
createAudioSource
|
||||
} = useWebDjSource();
|
||||
|
||||
const {
|
||||
sendMetadata
|
||||
} = useInjectWebcaster();
|
||||
|
||||
usePassthroughSync(trackPassThrough, props.id);
|
||||
|
||||
const fileIndex = ref(-1);
|
||||
|
|
|
@ -64,36 +64,161 @@
|
|||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label
|
||||
for="dj_username"
|
||||
class="mb-2"
|
||||
>
|
||||
{{ $gettext('Streamer/DJ Username') }}
|
||||
</label>
|
||||
<div class="controls">
|
||||
<input
|
||||
id="dj_username"
|
||||
v-model="djUsername"
|
||||
type="text"
|
||||
class="form-control"
|
||||
>
|
||||
<div class="form-row">
|
||||
<div class="col md-6">
|
||||
<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>
|
||||
<div class="col md-6">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label
|
||||
for="dj_password"
|
||||
class="mb-2"
|
||||
>
|
||||
{{ $gettext('Streamer/DJ Password') }}
|
||||
</label>
|
||||
<div class="controls">
|
||||
<input
|
||||
id="dj_password"
|
||||
v-model="djPassword"
|
||||
type="password"
|
||||
class="form-control"
|
||||
>
|
||||
<div class="form-row">
|
||||
<div class="col md-6">
|
||||
<div class="form-group">
|
||||
<label
|
||||
for="select_samplerate"
|
||||
class="mb-2"
|
||||
>
|
||||
{{ $gettext('Sample Rate') }}
|
||||
</label>
|
||||
<div class="controls">
|
||||
<select
|
||||
id="select_samplerate"
|
||||
v-model.number="sampleRate"
|
||||
class="form-control"
|
||||
>
|
||||
<option value="8000">
|
||||
8 kHz
|
||||
</option>
|
||||
<option value="11025">
|
||||
11.025 kHz
|
||||
</option>
|
||||
<option value="12000">
|
||||
12 kHz
|
||||
</option>
|
||||
<option value="16000">
|
||||
16 kHz
|
||||
</option>
|
||||
<option value="22050">
|
||||
22.05 kHz
|
||||
</option>
|
||||
<option value="24000">
|
||||
24 kHz
|
||||
</option>
|
||||
<option value="32000">
|
||||
32 kHz
|
||||
</option>
|
||||
<option value="44100">
|
||||
44.1 kHz
|
||||
</option>
|
||||
<option value="48000">
|
||||
48 kHz
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col md-6">
|
||||
<div class="form-group">
|
||||
<label
|
||||
for="select_bitrate"
|
||||
class="mb-2"
|
||||
>
|
||||
{{ $gettext('Bit Rate') }}
|
||||
</label>
|
||||
<div class="controls">
|
||||
<select
|
||||
id="select_bitrate"
|
||||
v-model.number="bitrate"
|
||||
class="form-control"
|
||||
>
|
||||
<option value="8">
|
||||
8 kbps
|
||||
</option>
|
||||
<option value="16">
|
||||
16 kbps
|
||||
</option>
|
||||
<option value="24">
|
||||
24 kbps
|
||||
</option>
|
||||
<option value="32">
|
||||
32 kbps
|
||||
</option>
|
||||
<option value="40">
|
||||
40 kbps
|
||||
</option>
|
||||
<option value="48">
|
||||
48 kbps
|
||||
</option>
|
||||
<option value="56">
|
||||
56 kbps
|
||||
</option>
|
||||
<option value="64">
|
||||
64 kbps
|
||||
</option>
|
||||
<option value="80">
|
||||
80 kbps
|
||||
</option>
|
||||
<option value="96">
|
||||
96 kbps
|
||||
</option>
|
||||
<option value="112">
|
||||
112 kbps
|
||||
</option>
|
||||
<option value="128">
|
||||
128 kbps
|
||||
</option>
|
||||
<option value="144">
|
||||
144 kbps
|
||||
</option>
|
||||
<option value="160">
|
||||
160 kbps
|
||||
</option>
|
||||
<option value="192">
|
||||
192 kbps
|
||||
</option>
|
||||
<option value="224">
|
||||
224 kbps
|
||||
</option>
|
||||
<option value="256">
|
||||
256 kbps
|
||||
</option>
|
||||
<option value="320">
|
||||
320 kbps
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -130,6 +255,7 @@ import {computed, ref, watch} from "vue";
|
|||
import {useTranslate} from "~/vendor/gettext";
|
||||
import {useInjectWebDjNode} from "~/components/Public/WebDJ/useWebDjNode";
|
||||
import {usePassthroughSync} from "~/components/Public/WebDJ/usePassthroughSync";
|
||||
import {useInjectWebcaster} from "~/components/Public/WebDJ/useWebcaster";
|
||||
|
||||
const props = defineProps({
|
||||
stationName: {
|
||||
|
@ -143,13 +269,18 @@ const djPassword = ref(null);
|
|||
|
||||
const {
|
||||
doPassThrough,
|
||||
isConnected,
|
||||
bitrate,
|
||||
sampleRate,
|
||||
startStream,
|
||||
stopStream,
|
||||
metadata,
|
||||
sendMetadata
|
||||
stopStream
|
||||
} = useInjectWebDjNode();
|
||||
|
||||
const {
|
||||
metadata,
|
||||
sendMetadata,
|
||||
isConnected
|
||||
} = useInjectWebcaster();
|
||||
|
||||
usePassthroughSync(doPassThrough, 'global');
|
||||
|
||||
const {$gettext} = useTranslate();
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {inject, provide, ref} from "vue";
|
||||
import {computed, inject, provide, ref} from "vue";
|
||||
|
||||
const injectKey = "webDjNode";
|
||||
|
||||
|
@ -11,55 +11,88 @@ export function useProvideWebDjNode(node) {
|
|||
}
|
||||
|
||||
export function useWebDjNode(webcaster) {
|
||||
const {isConnected, connect: connectSocket, metadata, sendMetadata} = webcaster;
|
||||
const {connect: connectSocket} = webcaster;
|
||||
|
||||
const doPassThrough = ref(false);
|
||||
|
||||
const context = new AudioContext({
|
||||
sampleRate: 44100
|
||||
const bitrate = ref(128);
|
||||
const sampleRate = ref(44100);
|
||||
const channelCount = ref(2);
|
||||
const bufferSize = ref(256);
|
||||
|
||||
const context = computed(() => {
|
||||
return new AudioContext({
|
||||
sampleRate: sampleRate.value
|
||||
});
|
||||
});
|
||||
|
||||
const sink = context.createScriptProcessor(256, 2, 2);
|
||||
const sink = computed(() => {
|
||||
let currentContext = context.value;
|
||||
|
||||
sink.onaudioprocess = (buf) => {
|
||||
for (let channel = 0; channel < buf.inputBuffer.numberOfChannels; channel++) {
|
||||
let channelData = buf.inputBuffer.getChannelData(channel);
|
||||
buf.outputBuffer.getChannelData(channel).set(channelData);
|
||||
}
|
||||
};
|
||||
let sink = currentContext.createScriptProcessor(
|
||||
bufferSize.value,
|
||||
channelCount.value,
|
||||
channelCount.value
|
||||
);
|
||||
|
||||
const passThrough = context.createScriptProcessor(256, 2, 2);
|
||||
|
||||
passThrough.onaudioprocess = (buf) => {
|
||||
for (let channel = 0; channel < buf.inputBuffer.numberOfChannels; channel++) {
|
||||
let channelData = buf.inputBuffer.getChannelData(channel);
|
||||
|
||||
if (doPassThrough.value) {
|
||||
sink.onaudioprocess = (buf) => {
|
||||
for (let channel = 0; channel < buf.inputBuffer.numberOfChannels; channel++) {
|
||||
let channelData = buf.inputBuffer.getChannelData(channel);
|
||||
buf.outputBuffer.getChannelData(channel).set(channelData);
|
||||
} else {
|
||||
buf.outputBuffer.getChannelData(channel).set(new Float32Array(channelData.length));
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
sink.connect(passThrough);
|
||||
passThrough.connect(context.destination);
|
||||
return sink;
|
||||
});
|
||||
|
||||
const streamNode = context.createMediaStreamDestination();
|
||||
streamNode.channelCount = 2;
|
||||
const passThrough = computed(() => {
|
||||
let currentContext = context.value;
|
||||
|
||||
sink.connect(streamNode);
|
||||
let passThrough = currentContext.createScriptProcessor(
|
||||
bufferSize.value,
|
||||
channelCount.value,
|
||||
channelCount.value
|
||||
);
|
||||
|
||||
passThrough.onaudioprocess = (buf) => {
|
||||
for (let channel = 0; channel < buf.inputBuffer.numberOfChannels; channel++) {
|
||||
let channelData = buf.inputBuffer.getChannelData(channel);
|
||||
|
||||
if (doPassThrough.value) {
|
||||
buf.outputBuffer.getChannelData(channel).set(channelData);
|
||||
} else {
|
||||
buf.outputBuffer.getChannelData(channel).set(new Float32Array(channelData.length));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
sink.value.connect(passThrough);
|
||||
passThrough.value.connect(currentContext.destination);
|
||||
|
||||
return passThrough;
|
||||
});
|
||||
|
||||
const streamNode = computed(() => {
|
||||
let currentContext = context.value;
|
||||
|
||||
const streamNode = currentContext.createMediaStreamDestination();
|
||||
streamNode.channelCount = channelCount.value;
|
||||
|
||||
sink.value.connect(streamNode);
|
||||
|
||||
return streamNode;
|
||||
});
|
||||
|
||||
let mediaRecorder;
|
||||
|
||||
const startStream = (username = null, password = null) => {
|
||||
context.resume();
|
||||
context.value.resume();
|
||||
|
||||
mediaRecorder = new MediaRecorder(
|
||||
streamNode.stream,
|
||||
streamNode.value.stream,
|
||||
{
|
||||
mimeType: "audio/webm;codecs=opus",
|
||||
audioBitsPerSecond: 128 * 1000
|
||||
audioBitsPerSecond: bitrate.value * 1000
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -72,77 +105,17 @@ export function useWebDjNode(webcaster) {
|
|||
mediaRecorder?.stop();
|
||||
};
|
||||
|
||||
const createAudioSource = ({file, audio}, cb, onEnd) => {
|
||||
const el = new Audio(URL.createObjectURL(file));
|
||||
el.controls = false;
|
||||
el.autoplay = false;
|
||||
el.loop = false;
|
||||
|
||||
let source = null;
|
||||
|
||||
el.addEventListener("ended", () => {
|
||||
if (typeof onEnd === "function") {
|
||||
onEnd();
|
||||
}
|
||||
});
|
||||
|
||||
el.addEventListener("canplay", () => {
|
||||
if (source) {
|
||||
return;
|
||||
}
|
||||
|
||||
source = context.createMediaElementSource(el);
|
||||
|
||||
source.play = () => el.play()
|
||||
source.position = () => el.currentTime;
|
||||
source.duration = () => el.duration;
|
||||
source.paused = () => el.paused;
|
||||
source.stop = () => {
|
||||
el.pause();
|
||||
return el.remove();
|
||||
};
|
||||
source.pause = () => el.pause;
|
||||
source.seek = (percent) => {
|
||||
let time = percent * parseFloat(audio.length);
|
||||
el.currentTime = time;
|
||||
return time;
|
||||
};
|
||||
|
||||
return cb(source);
|
||||
});
|
||||
};
|
||||
|
||||
const createMicrophoneSource = (audioDeviceId, cb) => {
|
||||
navigator.mediaDevices.getUserMedia({
|
||||
video: false,
|
||||
audio: {
|
||||
deviceId: audioDeviceId
|
||||
}
|
||||
}).then((stream) => {
|
||||
let source = context.createMediaStreamSource(stream);
|
||||
source.stop = () => {
|
||||
let ref = stream.getAudioTracks();
|
||||
return (ref !== null)
|
||||
? ref[0].stop()
|
||||
: 0;
|
||||
}
|
||||
|
||||
return cb(source);
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
doPassThrough,
|
||||
isConnected,
|
||||
bitrate,
|
||||
sampleRate,
|
||||
channelCount,
|
||||
bufferSize,
|
||||
context,
|
||||
sink,
|
||||
passThrough,
|
||||
streamNode,
|
||||
startStream,
|
||||
stopStream,
|
||||
createAudioSource,
|
||||
createMicrophoneSource,
|
||||
metadata,
|
||||
sendMetadata
|
||||
stopStream
|
||||
};
|
||||
}
|
||||
|
|
69
frontend/vue/components/Public/WebDJ/useWebDjSource.js
Normal file
69
frontend/vue/components/Public/WebDJ/useWebDjSource.js
Normal file
|
@ -0,0 +1,69 @@
|
|||
import {useInjectWebDjNode} from "~/components/Public/WebDJ/useWebDjNode";
|
||||
|
||||
export function useWebDjSource() {
|
||||
const {context} = useInjectWebDjNode();
|
||||
|
||||
const createAudioSource = ({file, audio}, cb, onEnd) => {
|
||||
const el = new Audio(URL.createObjectURL(file));
|
||||
el.controls = false;
|
||||
el.autoplay = false;
|
||||
el.loop = false;
|
||||
|
||||
let source = null;
|
||||
|
||||
el.addEventListener("ended", () => {
|
||||
if (typeof onEnd === "function") {
|
||||
onEnd();
|
||||
}
|
||||
});
|
||||
|
||||
el.addEventListener("canplay", () => {
|
||||
if (source) {
|
||||
return;
|
||||
}
|
||||
|
||||
source = context.value.createMediaElementSource(el);
|
||||
|
||||
source.play = () => el.play()
|
||||
source.position = () => el.currentTime;
|
||||
source.duration = () => el.duration;
|
||||
source.paused = () => el.paused;
|
||||
source.stop = () => {
|
||||
el.pause();
|
||||
return el.remove();
|
||||
};
|
||||
source.pause = () => el.pause;
|
||||
source.seek = (percent) => {
|
||||
let time = percent * parseFloat(audio.length);
|
||||
el.currentTime = time;
|
||||
return time;
|
||||
};
|
||||
|
||||
return cb(source);
|
||||
});
|
||||
};
|
||||
|
||||
const createMicrophoneSource = (audioDeviceId, cb) => {
|
||||
navigator.mediaDevices.getUserMedia({
|
||||
video: false,
|
||||
audio: {
|
||||
deviceId: audioDeviceId
|
||||
}
|
||||
}).then((stream) => {
|
||||
let source = context.value.createMediaStreamSource(stream);
|
||||
source.stop = () => {
|
||||
let ref = stream.getAudioTracks();
|
||||
return (ref !== null)
|
||||
? ref[0].stop()
|
||||
: 0;
|
||||
}
|
||||
|
||||
return cb(source);
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
createAudioSource,
|
||||
createMicrophoneSource
|
||||
}
|
||||
}
|
|
@ -5,9 +5,8 @@ export function useWebDjTrack() {
|
|||
const {
|
||||
context,
|
||||
sink,
|
||||
createMicrophoneSource,
|
||||
createAudioSource,
|
||||
sendMetadata
|
||||
channelCount,
|
||||
bufferSize
|
||||
} = useInjectWebDjNode();
|
||||
|
||||
const trackGain = ref(55);
|
||||
|
@ -18,11 +17,14 @@ export function useWebDjTrack() {
|
|||
let source = ref(null);
|
||||
|
||||
const createControlsNode = () => {
|
||||
const bufferSize = 4096;
|
||||
const bufferLog = Math.log(parseFloat(bufferSize));
|
||||
const bufferLog = Math.log(parseFloat(bufferSize.value));
|
||||
const log10 = 2.0 * Math.log(10);
|
||||
|
||||
let newSource = context.createScriptProcessor(bufferSize, 2, 2);
|
||||
let newSource = context.value.createScriptProcessor(
|
||||
bufferSize.value,
|
||||
channelCount.value,
|
||||
channelCount.value
|
||||
);
|
||||
|
||||
newSource.onaudioprocess = (buf) => {
|
||||
if (typeof (source.value?.position) === "function") {
|
||||
|
@ -47,7 +49,11 @@ export function useWebDjTrack() {
|
|||
};
|
||||
|
||||
const createPassThrough = () => {
|
||||
let newSource = context.createScriptProcessor(256, 2, 2);
|
||||
let newSource = context.value.createScriptProcessor(
|
||||
bufferSize.value,
|
||||
channelCount.value,
|
||||
channelCount.value
|
||||
);
|
||||
|
||||
newSource.onaudioprocess = (buf) => {
|
||||
for (let channel = 0; channel < buf.inputBuffer.numberOfChannels; channel++) {
|
||||
|
@ -79,17 +85,17 @@ export function useWebDjTrack() {
|
|||
|
||||
const prepare = () => {
|
||||
controlsNode = createControlsNode();
|
||||
controlsNode.connect(sink);
|
||||
controlsNode.connect(sink.value);
|
||||
|
||||
trackGainNode = context.createGain();
|
||||
trackGainNode = context.value.createGain();
|
||||
trackGainNode.gain.value = parseFloat(trackGain.value) / 100.0;
|
||||
trackGainNode.connect(controlsNode);
|
||||
|
||||
passThroughNode = createPassThrough();
|
||||
passThroughNode.connect(context.destination);
|
||||
passThroughNode.connect(context.value.destination);
|
||||
trackGainNode.connect(passThroughNode);
|
||||
|
||||
context.resume();
|
||||
context.value.resume();
|
||||
|
||||
return trackGainNode;
|
||||
}
|
||||
|
@ -130,9 +136,6 @@ export function useWebDjTrack() {
|
|||
};
|
||||
|
||||
return {
|
||||
createMicrophoneSource,
|
||||
createAudioSource,
|
||||
sendMetadata,
|
||||
source,
|
||||
trackGain,
|
||||
trackPassThrough,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {ref, shallowRef} from "vue";
|
||||
import {inject, provide, ref, shallowRef} from "vue";
|
||||
import {useNotify} from "~/vendor/bootstrapVue";
|
||||
import {useTranslate} from "~/vendor/gettext";
|
||||
|
||||
|
@ -9,6 +9,16 @@ export const webcasterProps = {
|
|||
}
|
||||
};
|
||||
|
||||
const injectKey = 'webDjWebcaster';
|
||||
|
||||
export function useProvideWebcaster(webcaster) {
|
||||
provide(injectKey, webcaster);
|
||||
}
|
||||
|
||||
export function useInjectWebcaster() {
|
||||
return inject(injectKey);
|
||||
}
|
||||
|
||||
export function useWebcaster(props) {
|
||||
const {baseUri} = props;
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user