WIP majority of refactor done.
This commit is contained in:
parent
48014dab6b
commit
b498288965
|
@ -1,2 +1,2 @@
|
|||
vue/components/Public/WebDJ/*
|
||||
vue/vendor/chartjs-colorschemes/*
|
||||
vue/vendor/webcast/*
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<div class="container pt-5">
|
||||
<div class="form-row">
|
||||
<div class="col-md-4 mb-sm-4">
|
||||
<settings-panel v-bind="{ stationName, baseUri, libUrls }" />
|
||||
<settings-panel :station-name="stationName" />
|
||||
</div>
|
||||
|
||||
<div class="col-md-8">
|
||||
|
@ -36,57 +36,25 @@
|
|||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
import MixerPanel from './WebDJ/MixerPanel.vue';
|
||||
import MicrophonePanel from './WebDJ/MicrophonePanel.vue';
|
||||
import PlaylistPanel from './WebDJ/PlaylistPanel.vue';
|
||||
import SettingsPanel from './WebDJ/SettingsPanel.vue';
|
||||
import {useWebDjNode} from "~/components/Public/WebDJ/useWebDjNode";
|
||||
import {provide} from "vue";
|
||||
import {useWebcaster, webcasterProps} from "~/components/Public/WebDJ/useWebcaster";
|
||||
import '~/vendor/webcast/taglib';
|
||||
|
||||
import Stream from './WebDJ/Stream.js';
|
||||
const props = defineProps({
|
||||
...webcasterProps,
|
||||
stationName: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
});
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MixerPanel,
|
||||
MicrophonePanel,
|
||||
PlaylistPanel,
|
||||
SettingsPanel
|
||||
},
|
||||
provide: function () {
|
||||
return {
|
||||
getStream: this.getStream,
|
||||
resumeStream: this.resumeStream
|
||||
};
|
||||
},
|
||||
props: {
|
||||
stationName: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
libUrls: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
baseUri: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
'stream': Stream
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getStream: function () {
|
||||
this.stream.init();
|
||||
|
||||
return this.stream;
|
||||
},
|
||||
resumeStream: function () {
|
||||
this.stream.resumeContext();
|
||||
}
|
||||
}
|
||||
};
|
||||
const webcaster = useWebcaster(props);
|
||||
const node = useWebDjNode(webcaster);
|
||||
provide('node', node);
|
||||
</script>
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
</h5>
|
||||
</div>
|
||||
<div class="flex-shrink-0 pl-3">
|
||||
<volume-slider v-model.number="volume" />
|
||||
<volume-slider v-model.number="trackGain" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -20,15 +20,15 @@
|
|||
<div class="btn-group btn-group-sm">
|
||||
<button
|
||||
class="btn btn-danger"
|
||||
:class="{ active: playing }"
|
||||
@click="toggleRecording"
|
||||
:class="{ active: isPlaying }"
|
||||
@click="togglePlaying"
|
||||
>
|
||||
<icon icon="mic" />
|
||||
</button>
|
||||
<button
|
||||
class="btn"
|
||||
:class="{ 'btn-primary': passThrough }"
|
||||
@click="cue"
|
||||
:class="{ 'btn-primary': trackPassThrough }"
|
||||
@click="trackPassThrough = !trackPassThrough"
|
||||
>
|
||||
{{ $gettext('Cue') }}
|
||||
</button>
|
||||
|
@ -50,7 +50,8 @@
|
|||
class="form-control"
|
||||
>
|
||||
<option
|
||||
v-for="device_row in devices"
|
||||
v-for="device_row in audioInputs"
|
||||
:key="device_row.deviceId"
|
||||
:value="device_row.deviceId"
|
||||
>
|
||||
{{ device_row.label }}
|
||||
|
@ -62,138 +63,67 @@
|
|||
</div>
|
||||
|
||||
<div
|
||||
v-if="playing"
|
||||
v-if="isPlaying"
|
||||
class="mt-3"
|
||||
>
|
||||
<div class="progress mb-1">
|
||||
<div
|
||||
class="progress-bar"
|
||||
:style="{ width: volumeLeft+'%' }"
|
||||
/>
|
||||
</div>
|
||||
<div class="progress mb-2">
|
||||
<div
|
||||
class="progress-bar"
|
||||
:style="{ width: volumeRight+'%' }"
|
||||
:style="{ width: volume+'%' }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import track from './Track.js';
|
||||
import {first, filter, isEmpty} from 'lodash';
|
||||
|
||||
<script setup>
|
||||
import Icon from '~/components/Common/Icon';
|
||||
import VolumeSlider from "~/components/Public/WebDJ/VolumeSlider";
|
||||
import {useDevicesList} from "@vueuse/core";
|
||||
import {ref, watch} from "vue";
|
||||
import {useWebDjTrack} from "~/components/Public/WebDJ/useWebDjTrack";
|
||||
|
||||
export default {
|
||||
components: {VolumeSlider, Icon},
|
||||
extends: track,
|
||||
const {node, isPlaying, trackGain, trackPassThrough, volume, prepare, stop} = useWebDjTrack();
|
||||
|
||||
data: function () {
|
||||
return {
|
||||
'device': null,
|
||||
'devices': [],
|
||||
'isRecording': false
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
device: function () {
|
||||
if (this.source == null) {
|
||||
return;
|
||||
}
|
||||
return this.createSource();
|
||||
}
|
||||
},
|
||||
mounted: function () {
|
||||
let base, base1;
|
||||
const {context, createMicrophoneSource} = node;
|
||||
|
||||
// Get multimedia devices by requesting them from the browser.
|
||||
navigator.mediaDevices || (navigator.mediaDevices = {});
|
||||
const {audioInputs} = useDevicesList({
|
||||
requestPermissions: true,
|
||||
});
|
||||
|
||||
(base = navigator.mediaDevices).getUserMedia || (base.getUserMedia = function (constraints) {
|
||||
let fn;
|
||||
fn = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia;
|
||||
if (fn == null) {
|
||||
return Promise.reject(new Error('getUserMedia is not implemented in this browser'));
|
||||
}
|
||||
return new Promise(function (resolve, reject) {
|
||||
return fn.call(navigator, constraints, resolve, reject);
|
||||
});
|
||||
});
|
||||
const device = ref(audioInputs.value[0]?.deviceId);
|
||||
|
||||
(base1 = navigator.mediaDevices).enumerateDevices || (base1.enumerateDevices = function () {
|
||||
return Promise.reject(new Error('enumerateDevices is not implemented on this browser'));
|
||||
});
|
||||
let source = null;
|
||||
|
||||
const vm_mic = this;
|
||||
navigator.mediaDevices.getUserMedia({
|
||||
audio: true,
|
||||
video: false
|
||||
}).then(function () {
|
||||
return navigator.mediaDevices.enumerateDevices().then(vm_mic.setDevices);
|
||||
});
|
||||
|
||||
this.$root.$on('new-cue', this.onNewCue);
|
||||
},
|
||||
methods: {
|
||||
cue: function () {
|
||||
this.resumeStream();
|
||||
this.$root.$emit('new-cue', (this.passThrough) ? 'off' : 'microphone');
|
||||
},
|
||||
onNewCue: function (new_cue) {
|
||||
this.passThrough = (new_cue === 'microphone');
|
||||
},
|
||||
toggleRecording: function () {
|
||||
this.resumeStream();
|
||||
|
||||
if (this.playing) {
|
||||
this.stop();
|
||||
} else {
|
||||
this.play();
|
||||
}
|
||||
},
|
||||
createSource: function (cb) {
|
||||
let constraints;
|
||||
if (this.source != null) {
|
||||
this.source.disconnect(this.destination);
|
||||
}
|
||||
constraints = {
|
||||
video: false
|
||||
};
|
||||
if (this.device) {
|
||||
constraints.audio = {
|
||||
deviceId: this.device
|
||||
};
|
||||
} else {
|
||||
constraints.audio = true;
|
||||
}
|
||||
return this.getStream().createMicrophoneSource(constraints, (source) => {
|
||||
this.source = source;
|
||||
this.source.connect(this.destination);
|
||||
return typeof cb === 'function' ? cb() : void 0;
|
||||
});
|
||||
},
|
||||
play: function () {
|
||||
this.prepare();
|
||||
|
||||
return this.createSource(() => {
|
||||
this.playing = true;
|
||||
this.paused = false;
|
||||
});
|
||||
},
|
||||
setDevices: function (devices) {
|
||||
devices = filter(devices, function ({kind}) {
|
||||
return kind === 'audioinput';
|
||||
});
|
||||
if (isEmpty(devices)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.devices = devices;
|
||||
this.device = first(devices).deviceId;
|
||||
}
|
||||
const createSource = () => {
|
||||
if (source != null) {
|
||||
source.disconnect(context.destination);
|
||||
}
|
||||
|
||||
createMicrophoneSource(device.value, (newSource) => {
|
||||
source = newSource;
|
||||
source.connect(context.destination);
|
||||
});
|
||||
};
|
||||
|
||||
watch(device, () => {
|
||||
if (this.source == null) {
|
||||
return;
|
||||
}
|
||||
createSource();
|
||||
});
|
||||
|
||||
const play = () => {
|
||||
prepare();
|
||||
createSource();
|
||||
}
|
||||
|
||||
const togglePlaying = () => {
|
||||
if (isPlaying.value) {
|
||||
stop();
|
||||
} else {
|
||||
play();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -14,14 +14,14 @@
|
|||
</div>
|
||||
<div class="flex-fill px-2">
|
||||
<input
|
||||
v-model="position"
|
||||
v-model="mixerValue"
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.01"
|
||||
class="custom-range slider"
|
||||
style="width: 200px; height: 10px;"
|
||||
@click.right.prevent="position = 0.5"
|
||||
@click.right.prevent="mixerValue = 0.5"
|
||||
>
|
||||
</div>
|
||||
<div class="flex-shrink-0">
|
||||
|
@ -34,17 +34,8 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
'position': 0.5
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
position(val) {
|
||||
this.$root.$emit('new-mixer-value', val);
|
||||
}
|
||||
}
|
||||
};
|
||||
<script setup>
|
||||
import {useMixerValue} from "~/components/Public/WebDJ/useMixerValue";
|
||||
|
||||
const mixerValue = useMixerValue();
|
||||
</script>
|
||||
|
|
|
@ -4,11 +4,11 @@
|
|||
<div class="d-flex align-items-center">
|
||||
<div class="flex-fill text-nowrap">
|
||||
<h5 class="card-title">
|
||||
{{ lang_header }}
|
||||
{{ langHeader }}
|
||||
</h5>
|
||||
</div>
|
||||
<div class="flex-shrink-0 pl-3">
|
||||
<volume-slider v-model.number="volume" />
|
||||
<volume-slider v-model.number="trackGain" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -16,14 +16,14 @@
|
|||
<div class="control-group d-flex justify-content-center">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button
|
||||
v-if="!playing || paused"
|
||||
v-if="!isPlaying || isPaused"
|
||||
class="btn btn-sm btn-success"
|
||||
@click="play"
|
||||
>
|
||||
<icon icon="play_arrow" />
|
||||
</button>
|
||||
<button
|
||||
v-if="playing && !paused"
|
||||
v-if="isPlaying && !isPaused"
|
||||
class="btn btn-sm btn-warning"
|
||||
@click="togglePause()"
|
||||
>
|
||||
|
@ -49,8 +49,8 @@
|
|||
</button>
|
||||
<button
|
||||
class="btn btn-sm"
|
||||
:class="{ 'btn-primary': passThrough }"
|
||||
@click="cue()"
|
||||
:class="{ 'btn-primary': trackPassThrough }"
|
||||
@click="trackPassThrough = !trackPassThrough"
|
||||
>
|
||||
{{ $gettext('Cue') }}
|
||||
</button>
|
||||
|
@ -58,7 +58,7 @@
|
|||
</div>
|
||||
|
||||
<div
|
||||
v-if="playing"
|
||||
v-if="isPlaying"
|
||||
class="mt-3"
|
||||
>
|
||||
<div class="d-flex flex-row mb-2">
|
||||
|
@ -72,10 +72,7 @@
|
|||
max="100"
|
||||
step="0.1"
|
||||
class="custom-range slider"
|
||||
:value="seekingPosition"
|
||||
@mousedown="isSeeking = true"
|
||||
@mousemove="doSeek($event)"
|
||||
@mouseup="isSeeking = false"
|
||||
:value="position"
|
||||
>
|
||||
</div>
|
||||
<div class="flex-shrink-0 pt-1 pl-2">
|
||||
|
@ -83,16 +80,10 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="progress mb-1">
|
||||
<div
|
||||
class="progress-bar"
|
||||
:style="{ width: volumeLeft+'%' }"
|
||||
/>
|
||||
</div>
|
||||
<div class="progress">
|
||||
<div
|
||||
class="progress-bar"
|
||||
:style="{ width: volumeRight+'%' }"
|
||||
:style="{ width: volume+'%' }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -156,6 +147,7 @@
|
|||
>
|
||||
<a
|
||||
v-for="(rowFile, rowIndex) in files"
|
||||
:key="rowFile.file.name"
|
||||
href="#"
|
||||
class="list-group-item list-group-item-action flex-column align-items-start"
|
||||
:class="{ active: rowIndex === fileIndex }"
|
||||
|
@ -163,199 +155,164 @@
|
|||
>
|
||||
<div class="d-flex w-100 justify-content-between">
|
||||
<h5 class="mb-0">{{
|
||||
rowFile?.metadata?.title ?? $gettext('Unknown Title')
|
||||
}}</h5>
|
||||
rowFile.metadata?.title ?? $gettext('Unknown Title')
|
||||
}}</h5>
|
||||
<small class="pt-1">{{ formatTime(rowFile.audio.length) }}</small>
|
||||
</div>
|
||||
<p class="mb-0">{{ rowFile?.metadata?.artist ?? $gettext('Unknown Artist') }}</p>
|
||||
<p class="mb-0">{{ rowFile.metadata?.artist ?? $gettext('Unknown Artist') }}</p>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import track from './Track.js';
|
||||
import {forEach} from 'lodash';
|
||||
<script setup>
|
||||
import Icon from '~/components/Common/Icon';
|
||||
import VolumeSlider from "~/components/Public/WebDJ/VolumeSlider";
|
||||
import formatTime from "../../../functions/formatTime";
|
||||
import formatTime from "~/functions/formatTime";
|
||||
import {computed, ref} from "vue";
|
||||
import {useWebDjTrack} from "~/components/Public/WebDJ/useWebDjTrack";
|
||||
import {useTranslate} from "~/vendor/gettext";
|
||||
import {forEach} from "lodash";
|
||||
|
||||
export default {
|
||||
components: {VolumeSlider, Icon},
|
||||
extends: track,
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
required: true
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const isLeftPlaylist = computed(() => {
|
||||
return props.id === 'playlist_1';
|
||||
});
|
||||
|
||||
const {
|
||||
node,
|
||||
isPlaying,
|
||||
isPaused,
|
||||
trackGain,
|
||||
trackPassThrough,
|
||||
position,
|
||||
volume,
|
||||
prepare,
|
||||
togglePause,
|
||||
stop
|
||||
} = useWebDjTrack();
|
||||
|
||||
const {context, createFileSource, updateMetadata} = node;
|
||||
|
||||
const fileIndex = ref(-1);
|
||||
const files = ref([]);
|
||||
const duration = ref(0.0);
|
||||
const loop = ref(false);
|
||||
const playThrough = ref(false);
|
||||
|
||||
const {$gettext} = useTranslate();
|
||||
|
||||
const langHeader = computed(() => {
|
||||
return isLeftPlaylist.value
|
||||
? this.$gettext('Playlist 1')
|
||||
: this.$gettext('Playlist 2');
|
||||
});
|
||||
|
||||
const addNewFiles = (newFiles) => {
|
||||
forEach(newFiles, (file) => {
|
||||
file.readTaglibMetadata((data) => {
|
||||
files.value.push({
|
||||
file: file,
|
||||
audio: data.audio,
|
||||
metadata: data.metadata || {title: '', artist: ''}
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
let source = null;
|
||||
|
||||
const selectFile = (options = {}) => {
|
||||
if (files.value.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.fileIndex) {
|
||||
fileIndex.value = options.fileIndex;
|
||||
} else {
|
||||
fileIndex.value += options.backward ? -1 : 1;
|
||||
if (fileIndex.value < 0) {
|
||||
fileIndex.value = files.value.length - 1;
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
'fileIndex': -1,
|
||||
'files': [],
|
||||
|
||||
'volume': 100,
|
||||
'duration': 0.0,
|
||||
'playThrough': true,
|
||||
'loop': false,
|
||||
if (fileIndex.value >= files.value.length) {
|
||||
if (options.isAutoPlay && !loop.value) {
|
||||
fileIndex.value = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
'isSeeking': false,
|
||||
'seekPosition': 0,
|
||||
'mixGainObj': null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
lang_header () {
|
||||
return (this.id === 'playlist_1')
|
||||
? this.$gettext('Playlist 1')
|
||||
: this.$gettext('Playlist 2');
|
||||
},
|
||||
positionPercent () {
|
||||
return (100.0 * this.position / parseFloat(this.duration));
|
||||
},
|
||||
seekingPosition () {
|
||||
return (this.isSeeking) ? this.seekPosition : this.positionPercent;
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.mixGainObj = this.getStream().context.createGain();
|
||||
this.mixGainObj.connect(this.getStream().webcast);
|
||||
this.sink = this.mixGainObj;
|
||||
|
||||
this.$root.$on('new-mixer-value', this.setMixGain);
|
||||
this.$root.$on('new-cue', this.onNewCue);
|
||||
},
|
||||
methods: {
|
||||
formatTime,
|
||||
cue() {
|
||||
this.resumeStream();
|
||||
this.$root.$emit('new-cue', (this.passThrough) ? 'off' : this.id);
|
||||
},
|
||||
onNewCue(new_cue) {
|
||||
this.passThrough = (new_cue === this.id);
|
||||
},
|
||||
setMixGain(new_value) {
|
||||
if (this.id === 'playlist_1') {
|
||||
this.mixGainObj.gain.value = 1.0 - new_value;
|
||||
if (fileIndex.value < 0) {
|
||||
fileIndex.value = files.value.length - 1;
|
||||
} else {
|
||||
this.mixGainObj.gain.value = new_value;
|
||||
}
|
||||
},
|
||||
addNewFiles (newFiles) {
|
||||
forEach(newFiles, (file) => {
|
||||
file.readTaglibMetadata((data) => {
|
||||
this.files.push({
|
||||
file: file,
|
||||
audio: data.audio,
|
||||
metadata: data.metadata || {title: '', artist: ''}
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
play (options) {
|
||||
this.resumeStream();
|
||||
|
||||
if (this.paused) {
|
||||
this.togglePause();
|
||||
return;
|
||||
}
|
||||
|
||||
this.stop();
|
||||
|
||||
if (!(this.file = this.selectFile(options))) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.prepare();
|
||||
|
||||
return this.getStream().createFileSource(this.file, this, (source) => {
|
||||
let ref1;
|
||||
this.source = source;
|
||||
this.source.connect(this.destination);
|
||||
if (this.source.duration != null) {
|
||||
this.duration = this.source.duration();
|
||||
} else {
|
||||
if (((ref1 = this.file.audio) != null ? ref1.length : void 0) != null) {
|
||||
this.duration = parseFloat(this.file.audio.length);
|
||||
}
|
||||
}
|
||||
|
||||
this.source.play(this.file);
|
||||
|
||||
this.$root.$emit('metadata-update', {
|
||||
title: this.file.metadata.title,
|
||||
artist: this.file.metadata.artist
|
||||
});
|
||||
|
||||
this.playing = true;
|
||||
this.paused = false;
|
||||
});
|
||||
},
|
||||
|
||||
selectFile (options = {}) {
|
||||
if (this.files.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.fileIndex) {
|
||||
this.fileIndex = options.fileIndex;
|
||||
} else {
|
||||
this.fileIndex += options.backward ? -1 : 1;
|
||||
if (this.fileIndex < 0) {
|
||||
this.fileIndex = this.files.length - 1;
|
||||
}
|
||||
|
||||
if (this.fileIndex >= this.files.length) {
|
||||
if (options.isAutoPlay && !this.loop) {
|
||||
this.fileIndex = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.fileIndex < 0) {
|
||||
this.fileIndex = this.files.length - 1;
|
||||
} else {
|
||||
this.fileIndex = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this.files[this.fileIndex];
|
||||
},
|
||||
|
||||
previous () {
|
||||
if (!this.playing) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.play({
|
||||
backward: true
|
||||
});
|
||||
},
|
||||
|
||||
next () {
|
||||
if (!this.playing) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.play();
|
||||
},
|
||||
|
||||
onEnd () {
|
||||
this.stop();
|
||||
|
||||
if (this.playThrough) {
|
||||
return this.play({
|
||||
isAutoPlay: true
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
doSeek (e) {
|
||||
if (this.isSeeking) {
|
||||
this.seekPosition = e.target.value;
|
||||
this.seek(this.seekPosition / 100);
|
||||
fileIndex.value = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return files.value[fileIndex.value];
|
||||
};
|
||||
|
||||
const play = (options = {}) => {
|
||||
let file = selectFile(options);
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isPaused.value) {
|
||||
togglePause();
|
||||
return;
|
||||
}
|
||||
|
||||
stop();
|
||||
|
||||
prepare();
|
||||
|
||||
createFileSource(file, (newSource) => {
|
||||
source = newSource;
|
||||
source.connect(context.destination);
|
||||
|
||||
if (source.duration !== null) {
|
||||
duration.value = source.duration();
|
||||
} else if (file.audio !== null) {
|
||||
duration.value = parseFloat(this.file.audio.length);
|
||||
}
|
||||
|
||||
source.play(file);
|
||||
|
||||
updateMetadata({
|
||||
title: this.file.metadata.title,
|
||||
artist: this.file.metadata.artist
|
||||
});
|
||||
}, () => {
|
||||
stop();
|
||||
|
||||
if (playThrough.value) {
|
||||
play({
|
||||
isAutoPlay: true
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const previous = () => {
|
||||
if (!isPlaying.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
play({backward: true});
|
||||
};
|
||||
|
||||
const next = () => {
|
||||
if (!isPlaying.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
play();
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -39,156 +39,6 @@
|
|||
id="settings"
|
||||
class="tab-pane active"
|
||||
>
|
||||
<div class="form-group">
|
||||
<label class="mb-2">
|
||||
{{ $gettext('Encoder') }}
|
||||
</label>
|
||||
<div class="controls">
|
||||
<div class="custom-control custom-radio custom-control-inline">
|
||||
<input
|
||||
id="encoder_mp3"
|
||||
v-model="encoder"
|
||||
type="radio"
|
||||
value="mp3"
|
||||
class="custom-control-input"
|
||||
>
|
||||
<label
|
||||
for="encoder_mp3"
|
||||
class="custom-control-label"
|
||||
>
|
||||
{{ $gettext('MP3') }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="custom-control custom-radio custom-control-inline">
|
||||
<input
|
||||
id="encoder_raw"
|
||||
v-model="encoder"
|
||||
type="radio"
|
||||
value="raw"
|
||||
class="custom-control-input"
|
||||
>
|
||||
<label
|
||||
for="encoder_raw"
|
||||
class="custom-control-label"
|
||||
>
|
||||
{{ $gettext('Raw') }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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 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 class="form-group">
|
||||
<label class="mb-2">
|
||||
{{ $gettext('DJ Credentials') }}
|
||||
|
@ -213,22 +63,6 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group mb-0">
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input
|
||||
id="use_async_worker"
|
||||
v-model="asynchronous"
|
||||
type="checkbox"
|
||||
class="custom-control-input"
|
||||
>
|
||||
<label
|
||||
for="use_async_worker"
|
||||
class="custom-control-label"
|
||||
>
|
||||
{{ $gettext('Use Asynchronous Worker') }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
id="metadata"
|
||||
|
@ -244,7 +78,7 @@
|
|||
<div class="controls">
|
||||
<input
|
||||
id="metadata_title"
|
||||
v-model="metadata.title"
|
||||
v-model="shownMetadata.title"
|
||||
class="form-control"
|
||||
type="text"
|
||||
:disabled="!isStreaming"
|
||||
|
@ -261,7 +95,7 @@
|
|||
<div class="controls">
|
||||
<input
|
||||
id="metadata_artist"
|
||||
v-model="metadata.artist"
|
||||
v-model="shownMetadata.artist"
|
||||
class="form-control"
|
||||
type="text"
|
||||
:disabled="!isStreaming"
|
||||
|
@ -287,21 +121,21 @@
|
|||
<button
|
||||
v-if="!isStreaming"
|
||||
class="btn btn-success"
|
||||
@click="startStreaming"
|
||||
@click="startStream(djUsername, djPassword)"
|
||||
>
|
||||
{{ langStreamButton }}
|
||||
</button>
|
||||
<button
|
||||
v-if="isStreaming"
|
||||
class="btn btn-danger"
|
||||
@click="stopStreaming"
|
||||
@click="stopStream"
|
||||
>
|
||||
{{ langStreamButton }}
|
||||
</button>
|
||||
<button
|
||||
class="btn"
|
||||
:class="{ 'btn-primary': passThrough }"
|
||||
@click="cue"
|
||||
:class="{ 'btn-primary': doPassThrough }"
|
||||
@click="doPassThrough = !doPassThrough"
|
||||
>
|
||||
{{ $gettext('Cue') }}
|
||||
</button>
|
||||
|
@ -309,129 +143,39 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
inject: ['getStream', 'resumeStream'],
|
||||
props: {
|
||||
stationName: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
libUrls: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
baseUri: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
'isStreaming': false,
|
||||
'djUsername': '',
|
||||
'djPassword': '',
|
||||
'bitrate': 256,
|
||||
'samplerate': 44100,
|
||||
'encoder': 'mp3',
|
||||
'asynchronous': true,
|
||||
'passThrough': false,
|
||||
'metadata': {
|
||||
'title': '',
|
||||
'artist': ''
|
||||
}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
langStreamButton () {
|
||||
return (this.isStreaming)
|
||||
? this.$gettext('Stop Streaming')
|
||||
: this.$gettext('Start Streaming');
|
||||
},
|
||||
uri () {
|
||||
return 'wss://' + this.djUsername + ':' + this.djPassword + '@' + this.baseUri;
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.$root.$on('new-cue', this.onNewCue);
|
||||
this.$root.$on('metadata-update', this.onMetadataUpdate);
|
||||
},
|
||||
methods: {
|
||||
cue () {
|
||||
this.resumeStream();
|
||||
<script setup>
|
||||
import {computed, inject, ref} from "vue";
|
||||
import {syncRef} from "@vueuse/core";
|
||||
|
||||
this.$root.$emit('new-cue', (this.passThrough) ? 'off' : 'master');
|
||||
},
|
||||
onNewCue (new_cue) {
|
||||
this.passThrough = (new_cue === 'master');
|
||||
this.getStream().webcast.setPassThrough(this.passThrough);
|
||||
},
|
||||
startStreaming () {
|
||||
this.resumeStream();
|
||||
|
||||
let encoderClass;
|
||||
switch (this.encoder) {
|
||||
case 'mp3':
|
||||
encoderClass = Webcast.Encoder.Mp3;
|
||||
break;
|
||||
case 'raw':
|
||||
encoderClass = Webcast.Encoder.Raw;
|
||||
}
|
||||
|
||||
let encoder = new encoderClass({
|
||||
channels: 2,
|
||||
samplerate: this.samplerate,
|
||||
bitrate: this.bitrate
|
||||
});
|
||||
|
||||
if (this.samplerate !== this.getStream().context.sampleRate) {
|
||||
encoder = new Webcast.Encoder.Resample({
|
||||
encoder: encoder,
|
||||
type: Samplerate.LINEAR,
|
||||
samplerate: this.getStream().context.sampleRate
|
||||
});
|
||||
}
|
||||
|
||||
if (this.asynchronous) {
|
||||
encoder = new Webcast.Encoder.Asynchronous({
|
||||
encoder: encoder,
|
||||
scripts: this.libUrls
|
||||
});
|
||||
}
|
||||
|
||||
let socket = this.getStream().webcast.connectSocket(encoder, this.uri);
|
||||
socket.addEventListener("open", () => {
|
||||
this.$notifySuccess(this.$gettext('Live stream connected.'));
|
||||
this.isStreaming = true;
|
||||
this.updateMetadata(false);
|
||||
});
|
||||
socket.addEventListener("close", () => {
|
||||
this.$notifyError(this.$gettext('Live stream disconnected.'));
|
||||
this.isStreaming = false;
|
||||
});
|
||||
},
|
||||
stopStreaming () {
|
||||
this.getStream().webcast.close();
|
||||
this.isStreaming = false;
|
||||
},
|
||||
updateMetadata(alert = true) {
|
||||
this.$root.$emit('metadata-update', {
|
||||
title: this.metadata.title,
|
||||
artist: this.metadata.artist
|
||||
});
|
||||
|
||||
if (alert) {
|
||||
this.$notifySuccess(this.$gettext('Metadata updated!'));
|
||||
}
|
||||
},
|
||||
onMetadataUpdate (new_metadata) {
|
||||
this.metadata.title = new_metadata.title;
|
||||
this.metadata.artist = new_metadata.artist;
|
||||
|
||||
return this.getStream().webcast.sendMetadata(new_metadata);
|
||||
}
|
||||
const props = defineProps({
|
||||
stationName: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const djUsername = ref(null);
|
||||
const djPassword = ref(null);
|
||||
|
||||
const {
|
||||
doPassThrough,
|
||||
isStreaming,
|
||||
startStream,
|
||||
stopStream,
|
||||
metadata,
|
||||
sendMetadata
|
||||
} = inject('node');
|
||||
|
||||
const langStreamButton = computed(() => {
|
||||
return (isStreaming.value)
|
||||
? this.$gettext('Stop Streaming')
|
||||
: this.$gettext('Start Streaming');
|
||||
});
|
||||
|
||||
const shownMetadata = ref({});
|
||||
syncRef(metadata, shownMetadata, {direction: "ltr"});
|
||||
|
||||
const updateMetadata = () => {
|
||||
sendMetadata(shownMetadata.value);
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -1,106 +0,0 @@
|
|||
// noinspection all
|
||||
// @eslint-
|
||||
|
||||
var stream = {};
|
||||
var defaultChannels = 2;
|
||||
|
||||
// Function to be called upon the first user interaction.
|
||||
stream.init = function () {
|
||||
// Define the streaming radio context.
|
||||
if (!this.context) {
|
||||
if (typeof webkitAudioContext !== 'undefined') {
|
||||
this.context = new webkitAudioContext;
|
||||
} else {
|
||||
this.context = new AudioContext;
|
||||
}
|
||||
|
||||
this.webcast = this.context.createWebcastSource(4096, defaultChannels);
|
||||
this.webcast.connect(this.context.destination);
|
||||
}
|
||||
};
|
||||
|
||||
stream.resumeContext = function () {
|
||||
if (this.context.state !== 'running') {
|
||||
this.context.resume();
|
||||
}
|
||||
};
|
||||
|
||||
stream.createAudioSource = function ({
|
||||
file,
|
||||
audio
|
||||
}, model, cb) {
|
||||
var el,
|
||||
source;
|
||||
|
||||
el = new Audio(URL.createObjectURL(file));
|
||||
el.controls = false;
|
||||
el.autoplay = false;
|
||||
el.loop = false;
|
||||
|
||||
el.addEventListener('ended', function () {
|
||||
return model.onEnd();
|
||||
});
|
||||
|
||||
source = null;
|
||||
return el.addEventListener('canplay', function () {
|
||||
if (source != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
source = stream.context.createMediaElementSource(el);
|
||||
source.play = function () {
|
||||
return el.play();
|
||||
};
|
||||
source.position = function () {
|
||||
return el.currentTime;
|
||||
};
|
||||
source.duration = function () {
|
||||
return el.duration;
|
||||
};
|
||||
source.paused = function () {
|
||||
return el.paused;
|
||||
};
|
||||
source.stop = function () {
|
||||
el.pause();
|
||||
return el.remove();
|
||||
};
|
||||
source.pause = function () {
|
||||
return el.pause();
|
||||
};
|
||||
source.seek = function (percent) {
|
||||
var time;
|
||||
time = percent * parseFloat(audio.length);
|
||||
el.currentTime = time;
|
||||
return time;
|
||||
};
|
||||
|
||||
return cb(source);
|
||||
});
|
||||
};
|
||||
|
||||
stream.createFileSource = function (file, model, cb) {
|
||||
var ref;
|
||||
if ((ref = this.source) != null) {
|
||||
ref.disconnect();
|
||||
}
|
||||
return this.createAudioSource(file, model, cb);
|
||||
};
|
||||
|
||||
stream.createMicrophoneSource = function (constraints, cb) {
|
||||
return navigator.mediaDevices.getUserMedia(constraints).then(function (bit_stream) {
|
||||
var source;
|
||||
|
||||
source = stream.context.createMediaStreamSource(bit_stream);
|
||||
source.stop = function () {
|
||||
var ref;
|
||||
return (ref = bit_stream.getAudioTracks()) != null ? ref[0].stop() : void 0;
|
||||
};
|
||||
return cb(source);
|
||||
});
|
||||
};
|
||||
|
||||
stream.close = function (cb) {
|
||||
return this.webcast.close(cb);
|
||||
};
|
||||
|
||||
export default stream;
|
|
@ -1,222 +0,0 @@
|
|||
// noinspection all
|
||||
|
||||
export default {
|
||||
inject: ['getStream', 'resumeStream'],
|
||||
data: function () {
|
||||
return {
|
||||
'controlsNode': null,
|
||||
|
||||
'trackGain': 0,
|
||||
'trackGainObj': null,
|
||||
|
||||
'destination': null,
|
||||
'sink': null,
|
||||
|
||||
'passThrough': false,
|
||||
'passThroughObj': null,
|
||||
|
||||
'source': null,
|
||||
'playing': false,
|
||||
'paused': false,
|
||||
'position': 0.0,
|
||||
'volume': 100,
|
||||
'volumeLeft': 0,
|
||||
'volumeRight': 0
|
||||
};
|
||||
},
|
||||
mounted: function () {
|
||||
this.sink = this.getStream().webcast;
|
||||
},
|
||||
watch: {
|
||||
volume: function (val, oldVal) {
|
||||
this.setTrackGain(val);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
createControlsNode: function () {
|
||||
var bufferLength,
|
||||
bufferLog,
|
||||
bufferSize,
|
||||
log10,
|
||||
source;
|
||||
|
||||
bufferSize = 4096;
|
||||
bufferLength = parseFloat(bufferSize) / parseFloat(this.getStream().context.sampleRate);
|
||||
bufferLog = Math.log(parseFloat(bufferSize));
|
||||
log10 = 2.0 * Math.log(10);
|
||||
|
||||
source = this.getStream().context.createScriptProcessor(bufferSize, 2, 2);
|
||||
|
||||
source.onaudioprocess = (buf) => {
|
||||
var channel,
|
||||
channelData,
|
||||
i,
|
||||
j,
|
||||
k,
|
||||
ref1,
|
||||
ref2,
|
||||
ref3,
|
||||
results,
|
||||
ret,
|
||||
rms,
|
||||
volume;
|
||||
ret = {};
|
||||
|
||||
if (((ref1 = this.source) != null ? ref1.position : void 0) != null) {
|
||||
this.position = this.source.position();
|
||||
} else {
|
||||
if (this.source != null) {
|
||||
this.position = parseFloat(this.position) + bufferLength;
|
||||
}
|
||||
}
|
||||
results = [];
|
||||
for (channel = j = 0, ref2 = buf.inputBuffer.numberOfChannels - 1; (0 <= ref2 ? j <= ref2 : j >= ref2); channel = 0 <= ref2 ? ++j : --j) {
|
||||
channelData = buf.inputBuffer.getChannelData(channel);
|
||||
rms = 0.0;
|
||||
for (i = k = 0, ref3 = channelData.length - 1; (0 <= ref3 ? k <= ref3 : k >= ref3); i = 0 <= ref3 ? ++k : --k) {
|
||||
rms += Math.pow(channelData[i], 2);
|
||||
}
|
||||
volume = 100 * Math.exp((Math.log(rms) - bufferLog) / log10);
|
||||
if (channel === 0) {
|
||||
this.volumeLeft = volume;
|
||||
} else {
|
||||
this.volumeRight = volume;
|
||||
}
|
||||
|
||||
results.push(buf.outputBuffer.getChannelData(channel).set(channelData));
|
||||
}
|
||||
return results;
|
||||
};
|
||||
return source;
|
||||
},
|
||||
|
||||
createPassThrough: function () {
|
||||
var source;
|
||||
source = this.getStream().context.createScriptProcessor(256, 2, 2);
|
||||
source.onaudioprocess = (buf) => {
|
||||
var channel,
|
||||
channelData,
|
||||
j,
|
||||
ref1,
|
||||
results;
|
||||
channelData = buf.inputBuffer.getChannelData(channel);
|
||||
results = [];
|
||||
for (channel = j = 0, ref1 = buf.inputBuffer.numberOfChannels - 1; (0 <= ref1 ? j <= ref1 : j >= ref1); channel = 0 <= ref1 ? ++j : --j) {
|
||||
if (this.passThrough) {
|
||||
results.push(buf.outputBuffer.getChannelData(channel).set(channelData));
|
||||
} else {
|
||||
results.push(buf.outputBuffer.getChannelData(channel).set(new Float32Array(channelData.length)));
|
||||
}
|
||||
}
|
||||
return results;
|
||||
};
|
||||
return source;
|
||||
},
|
||||
|
||||
setTrackGain: function (new_gain) {
|
||||
return (this.trackGainObj) && (this.trackGainObj.gain.value = parseFloat(new_gain) / 100.0);
|
||||
},
|
||||
|
||||
togglePause: function () {
|
||||
var ref1,
|
||||
ref2;
|
||||
if (((ref1 = this.source) != null ? ref1.pause : void 0) == null) {
|
||||
return;
|
||||
}
|
||||
if ((ref2 = this.source) != null ? typeof ref2.paused === 'function' ? ref2.paused() : void 0 : void 0) {
|
||||
this.source.play();
|
||||
this.playing = true;
|
||||
this.paused = false;
|
||||
} else {
|
||||
this.source.pause();
|
||||
this.playing = false;
|
||||
this.paused = true;
|
||||
}
|
||||
},
|
||||
|
||||
prepare: function () {
|
||||
this.controlsNode = this.createControlsNode();
|
||||
this.controlsNode.connect(this.sink);
|
||||
|
||||
this.trackGainObj = this.getStream().context.createGain();
|
||||
this.trackGainObj.connect(this.controlsNode);
|
||||
this.trackGainObj.gain.value = 1.0;
|
||||
|
||||
this.destination = this.trackGainObj;
|
||||
|
||||
this.passThroughObj = this.createPassThrough();
|
||||
this.passThroughObj.connect(this.getStream().context.destination);
|
||||
|
||||
return this.trackGainObj.connect(this.passThroughObj);
|
||||
},
|
||||
|
||||
stop: function () {
|
||||
var ref1,
|
||||
ref2,
|
||||
ref3,
|
||||
ref4,
|
||||
ref5;
|
||||
if ((ref1 = this.source) != null) {
|
||||
if (typeof ref1.stop === 'function') {
|
||||
ref1.stop();
|
||||
}
|
||||
}
|
||||
if ((ref2 = this.source) != null) {
|
||||
ref2.disconnect();
|
||||
}
|
||||
if ((ref3 = this.trackGainObj) != null) {
|
||||
ref3.disconnect();
|
||||
}
|
||||
if ((ref4 = this.controlsNode) != null) {
|
||||
ref4.disconnect();
|
||||
}
|
||||
if ((ref5 = this.passThroughObj) != null) {
|
||||
ref5.disconnect();
|
||||
}
|
||||
this.source = this.trackGainObj = this.controlsNode = this.passThroughObj = null;
|
||||
|
||||
this.position = 0.0;
|
||||
this.volumeLeft = 0;
|
||||
this.volumeRight = 0;
|
||||
|
||||
this.playing = false;
|
||||
this.paused = false;
|
||||
},
|
||||
|
||||
seek: function (percent) {
|
||||
var position,
|
||||
ref1;
|
||||
if (!(position = (ref1 = this.source) != null ? typeof ref1.seek === 'function' ? ref1.seek(percent) : void 0 : void 0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.position = position;
|
||||
},
|
||||
|
||||
prettifyTime: function (time) {
|
||||
var hours,
|
||||
minutes,
|
||||
result,
|
||||
seconds;
|
||||
hours = parseInt(time / 3600);
|
||||
time %= 3600;
|
||||
minutes = parseInt(time / 60);
|
||||
seconds = parseInt(time % 60);
|
||||
if (minutes < 10) {
|
||||
minutes = `0${minutes}`;
|
||||
}
|
||||
if (seconds < 10) {
|
||||
seconds = `0${seconds}`;
|
||||
}
|
||||
result = `${minutes}:${seconds}`;
|
||||
if (hours > 0) {
|
||||
result = `${hours}:${result}`;
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
sendMetadata: function (file) {
|
||||
this.getStream().webcast.sendMetadata(file.metadata);
|
||||
}
|
||||
}
|
||||
};
|
|
@ -0,0 +1,8 @@
|
|||
import {createGlobalState} from "@vueuse/core";
|
||||
import {ref} from "vue";
|
||||
|
||||
export function useMixerValue() {
|
||||
return createGlobalState(
|
||||
() => ref(0.5)
|
||||
);
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
import {ref} from "vue";
|
||||
import Webcast from "~/vendor/webcast/webcast";
|
||||
import {useUserMedia} from "@vueuse/core";
|
||||
|
||||
export function useWebDjNode() {
|
||||
const doPlayThrough = ref(false);
|
||||
export function useWebDjNode(webcaster) {
|
||||
const doPassThrough = ref(false);
|
||||
const isStreaming = ref(false);
|
||||
|
||||
const context = new AudioContext({
|
||||
|
@ -12,22 +12,19 @@ export function useWebDjNode() {
|
|||
const sink = context.createScriptProcessor(256, 2, 2);
|
||||
|
||||
sink.onaudioprocess((buf) => {
|
||||
let channel;
|
||||
let channelData = buf.inputBuffer.getChannelData(channel);
|
||||
|
||||
for (channel = 0; channel < buf.inputBuffer.numberOfChannels - 1; channel++) {
|
||||
for (let channel = 0; channel < buf.inputBuffer.numberOfChannels - 1; channel++) {
|
||||
let channelData = buf.inputBuffer.getChannelData(channel);
|
||||
buf.outputBuffer.getChannelData(channel).set(channelData);
|
||||
}
|
||||
});
|
||||
|
||||
const playThrough = context.createScriptProcessor(256, 2, 2);
|
||||
const passThrough = context.createScriptProcessor(256, 2, 2);
|
||||
|
||||
playThrough.onaudioprocess((buf) => {
|
||||
let channel;
|
||||
let channelData = buf.inputBuffer.getChannelData(channel);
|
||||
passThrough.onaudioprocess((buf) => {
|
||||
for (let channel = 0; channel < buf.inputBuffer.numberOfChannels - 1; channel++) {
|
||||
let channelData = buf.inputBuffer.getChannelData(channel);
|
||||
|
||||
for (channel = 0; channel < buf.inputBuffer.numberOfChannels - 1; channel++) {
|
||||
if (doPlayThrough.value) {
|
||||
if (doPassThrough.value) {
|
||||
buf.outputBuffer.getChannelData(channel).set(channelData);
|
||||
} else {
|
||||
buf.outputBuffer.getChannelData(channel).set(new Float32Array(channelData.length));
|
||||
|
@ -35,18 +32,17 @@ export function useWebDjNode() {
|
|||
}
|
||||
});
|
||||
|
||||
sink.connect(playThrough);
|
||||
playThrough.connect(context.destination);
|
||||
sink.connect(passThrough);
|
||||
passThrough.connect(context.destination);
|
||||
|
||||
const streamNode = context.createMediaStreamDestination();
|
||||
streamNode.channelCount = 2;
|
||||
|
||||
sink.connect(streamNode);
|
||||
|
||||
let socket;
|
||||
let mediaRecorder;
|
||||
|
||||
const startStream = (url) => {
|
||||
const startStream = (username = null, password = null) => {
|
||||
isStreaming.value = true;
|
||||
|
||||
context.resume();
|
||||
|
@ -55,16 +51,11 @@ export function useWebDjNode() {
|
|||
streamNode.stream,
|
||||
{
|
||||
mimeType: "audio/webm;codecs=opus",
|
||||
audioBitsPerSecond: 128
|
||||
audioBitsPerSecond: 128 * 1000
|
||||
}
|
||||
);
|
||||
|
||||
socket = new Webcast.Socket(
|
||||
mediaRecorder,
|
||||
{
|
||||
url: url
|
||||
}
|
||||
);
|
||||
webcaster.connect(mediaRecorder, username, password);
|
||||
|
||||
mediaRecorder.start(1000);
|
||||
}
|
||||
|
@ -74,7 +65,7 @@ export function useWebDjNode() {
|
|||
isStreaming.value = false;
|
||||
};
|
||||
|
||||
const createAudioSource = ({file, audio}, model, cb) => {
|
||||
const createAudioSource = ({file, audio}, cb, onEnd) => {
|
||||
const el = new Audio(URL.createObjectURL(file));
|
||||
el.controls = false;
|
||||
el.autoplay = false;
|
||||
|
@ -82,6 +73,12 @@ export function useWebDjNode() {
|
|||
|
||||
let source = null;
|
||||
|
||||
el.addEventListener("ended", () => {
|
||||
if (typeof onEnd === "function") {
|
||||
onEnd();
|
||||
}
|
||||
});
|
||||
|
||||
el.addEventListener("canplay", () => {
|
||||
if (source) {
|
||||
return;
|
||||
|
@ -108,41 +105,45 @@ export function useWebDjNode() {
|
|||
});
|
||||
};
|
||||
|
||||
const createFileSource = (file, model, cb) => {
|
||||
source?.disconnect();
|
||||
|
||||
return createAudioSource(file, model, cb);
|
||||
const createFileSource = (file, cb) => {
|
||||
return createAudioSource(file, cb);
|
||||
};
|
||||
|
||||
const createMicrophoneSource = (constraints, cb) => {
|
||||
navigator.mediaDevices.getUserMedia(constraints).then((stream) => {
|
||||
let source = context.createMediaStreamSource(stream);
|
||||
source.stop = () => {
|
||||
let ref = stream.getAudioTracks();
|
||||
return (ref !== null)
|
||||
? ref[0].stop()
|
||||
: 0;
|
||||
}
|
||||
|
||||
return cb(source);
|
||||
const createMicrophoneSource = (audioDeviceId, cb) => {
|
||||
const {stream} = useUserMedia({
|
||||
audioDeviceId: audioDeviceId,
|
||||
});
|
||||
|
||||
stream.stop = () => {
|
||||
let ref = stream.getAudioTracks();
|
||||
return (ref !== null)
|
||||
? ref[0].stop()
|
||||
: 0;
|
||||
}
|
||||
|
||||
return cb(stream);
|
||||
};
|
||||
|
||||
const metadata = ref({});
|
||||
|
||||
const sendMetadata = (data) => {
|
||||
socket?.sendMetadata(data);
|
||||
webcaster.sendMetadata(data);
|
||||
metadata.value = data;
|
||||
};
|
||||
|
||||
return {
|
||||
doPassThrough,
|
||||
isStreaming,
|
||||
context,
|
||||
sink,
|
||||
doPlayThrough,
|
||||
playThrough,
|
||||
passThrough,
|
||||
streamNode,
|
||||
startStream,
|
||||
stopStream,
|
||||
createAudioSource,
|
||||
createFileSource,
|
||||
createMicrophoneSource,
|
||||
metadata,
|
||||
sendMetadata
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,132 @@
|
|||
import {computed, inject, ref, watch} from "vue";
|
||||
|
||||
export function useWebDjTrack() {
|
||||
const node = inject('node');
|
||||
|
||||
const trackGain = ref(55);
|
||||
const trackPassThrough = ref(false);
|
||||
const position = ref(null);
|
||||
const volume = ref(0);
|
||||
|
||||
let source = null;
|
||||
|
||||
const isPlaying = computed(() => {
|
||||
return source !== null;
|
||||
});
|
||||
|
||||
const isPaused = computed(() => {
|
||||
return (source !== null)
|
||||
? source.paused
|
||||
: false;
|
||||
});
|
||||
|
||||
const createControlsNode = () => {
|
||||
const bufferSize = 4096;
|
||||
const bufferLog = Math.log(parseFloat(bufferSize));
|
||||
const log10 = 2.0 * Math.log(10);
|
||||
|
||||
let newSource = node.context.createScriptProcessor(bufferSize, 2, 2);
|
||||
|
||||
newSource.onaudioprocess((buf) => {
|
||||
position.value = source?.position();
|
||||
|
||||
for (let channel = 0; channel < buf.inputBuffer.numberOfChannels; channel++) {
|
||||
let channelData = buf.inputBuffer.getChannelData(channel);
|
||||
|
||||
let rms = 0.0;
|
||||
for (let i = 0; i < channelData.length; i++) {
|
||||
rms += Math.pow(channelData[i], 2);
|
||||
}
|
||||
|
||||
volume.value = 100 * Math.exp((Math.log(rms) - bufferLog) / log10);
|
||||
|
||||
buf.outputBuffer.getChannelData(channel).set(channelData);
|
||||
}
|
||||
});
|
||||
|
||||
return newSource;
|
||||
};
|
||||
|
||||
const createPassThrough = () => {
|
||||
let newSource = node.context.createScriptProcessor(256, 2, 2);
|
||||
|
||||
newSource.onaudioprocess((buf) => {
|
||||
for (let channel = 0; channel < buf.inputBuffer.numberOfChannels; channel++) {
|
||||
let channelData = buf.inputBuffer.getChannelData(channel);
|
||||
|
||||
if (trackPassThrough.value) {
|
||||
buf.outputBuffer.getChannelData(channel).set(channelData);
|
||||
} else {
|
||||
buf.outputBuffer.getChannelData(channel).set(new Float32Array(channelData.length));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return newSource;
|
||||
};
|
||||
|
||||
let controlsNode = null;
|
||||
let trackGainNode = null;
|
||||
let passThroughNode = null;
|
||||
|
||||
const setTrackGain = (newGain) => {
|
||||
if (null === trackGainNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
trackGainNode.gain.value = parseFloat(newGain) / 100.0;
|
||||
};
|
||||
watch(trackGain, setTrackGain);
|
||||
|
||||
const prepare = () => {
|
||||
controlsNode = createControlsNode();
|
||||
controlsNode.connect(node.sink);
|
||||
|
||||
trackGainNode = node.context.createGain();
|
||||
trackGainNode.connect(controlsNode);
|
||||
|
||||
passThroughNode = createPassThrough();
|
||||
passThroughNode.connect(node.context.destination);
|
||||
trackGainNode.connect(passThroughNode);
|
||||
|
||||
node.context.resume();
|
||||
}
|
||||
|
||||
const togglePause = () => {
|
||||
if (source === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (source.paused) {
|
||||
source.play();
|
||||
} else {
|
||||
source.pause();
|
||||
}
|
||||
};
|
||||
|
||||
const stop = () => {
|
||||
source?.stop();
|
||||
source?.disconnect();
|
||||
|
||||
trackGainNode?.disconnect();
|
||||
controlsNode?.disconnect();
|
||||
passThroughNode?.disconnect();
|
||||
|
||||
source = trackGainNode = controlsNode = passThroughNode = null;
|
||||
|
||||
position.value = 0.0;
|
||||
};
|
||||
|
||||
return {
|
||||
node,
|
||||
trackGain,
|
||||
trackPassThrough,
|
||||
position,
|
||||
volume,
|
||||
isPlaying,
|
||||
isPaused,
|
||||
prepare,
|
||||
togglePause,
|
||||
stop,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
export const webcasterProps = {
|
||||
baseUri: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
};
|
||||
|
||||
export function useWebcaster(props) {
|
||||
const {baseUri} = props;
|
||||
|
||||
let socket = null;
|
||||
let mediaRecorder = null;
|
||||
|
||||
const isConnected = () => {
|
||||
return socket !== null && socket.readyState === WebSocket.OPEN;
|
||||
};
|
||||
|
||||
const connect = (newMediaRecorder, username = null, password = null) => {
|
||||
socket = new WebSocket(baseUri, "webcast");
|
||||
mediaRecorder = newMediaRecorder;
|
||||
|
||||
let hello = {
|
||||
mime: mediaRecorder.mimeType,
|
||||
};
|
||||
|
||||
if (null !== username) {
|
||||
hello.username = username;
|
||||
}
|
||||
if (null !== password) {
|
||||
hello.password = password;
|
||||
}
|
||||
|
||||
socket.onopen = () => {
|
||||
socket.send(JSON.stringify({
|
||||
type: "hello",
|
||||
data: hello
|
||||
}))
|
||||
};
|
||||
|
||||
mediaRecorder.ondataavailable = async (e) => {
|
||||
const data = await e.data.arrayBuffer();
|
||||
if (isConnected()) {
|
||||
socket.send(data);
|
||||
}
|
||||
};
|
||||
|
||||
mediaRecorder.onstop = () => {
|
||||
if (isConnected()) {
|
||||
socket.close();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const sendMetadata = (data) => {
|
||||
socket.send(JSON.stringify({
|
||||
type: "metadata",
|
||||
data,
|
||||
}));
|
||||
}
|
||||
|
||||
return {
|
||||
isConnected,
|
||||
connect,
|
||||
sendMetadata
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
|
@ -1,21 +0,0 @@
|
|||
export declare const version = "1.0.1";
|
||||
export declare class Socket {
|
||||
socket: WebSocket;
|
||||
constructor({ mediaRecorder, url: rawUrl, info, onError, onOpen, }: {
|
||||
mediaRecorder: MediaRecorder;
|
||||
url: string;
|
||||
info: Record<string, unknown>;
|
||||
onError?: (_: Event) => void;
|
||||
onOpen?: (_: Event) => void;
|
||||
});
|
||||
isConnected(): boolean;
|
||||
sendMetadata(data: Record<string, unknown>): void;
|
||||
}
|
||||
declare global {
|
||||
interface Window {
|
||||
Webcast: {
|
||||
Socket: typeof Socket;
|
||||
version: string;
|
||||
};
|
||||
}
|
||||
}
|
|
@ -1,62 +0,0 @@
|
|||
"use strict";
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.Socket = exports.version = void 0;
|
||||
exports.version = "1.0.1";
|
||||
class Socket {
|
||||
constructor({ mediaRecorder, url: rawUrl, info, onError, onOpen, }) {
|
||||
const parser = document.createElement("a");
|
||||
parser.href = rawUrl;
|
||||
const user = parser.username;
|
||||
const password = parser.password;
|
||||
parser.username = parser.password = "";
|
||||
const url = parser.href;
|
||||
this.socket = new WebSocket(url, "webcast");
|
||||
if (onError)
|
||||
this.socket.onerror = onError;
|
||||
const hello = Object.assign(Object.assign(Object.assign({ mime: mediaRecorder.mimeType }, (user ? { user } : {})), (password ? { password } : {})), info);
|
||||
this.socket.onopen = function onopen(event) {
|
||||
if (onOpen)
|
||||
onOpen(event);
|
||||
this.send(JSON.stringify({
|
||||
type: "hello",
|
||||
data: hello,
|
||||
}));
|
||||
};
|
||||
mediaRecorder.ondataavailable = (e) => __awaiter(this, void 0, void 0, function* () {
|
||||
const data = yield e.data.arrayBuffer();
|
||||
if (this.isConnected()) {
|
||||
this.socket.send(data);
|
||||
}
|
||||
});
|
||||
mediaRecorder.onstop = (e) => {
|
||||
if (this.isConnected()) {
|
||||
this.socket.close();
|
||||
}
|
||||
};
|
||||
}
|
||||
isConnected() {
|
||||
return this.socket.readyState === WebSocket.OPEN;
|
||||
}
|
||||
sendMetadata(data) {
|
||||
this.socket.send(JSON.stringify({
|
||||
type: "metadata",
|
||||
data,
|
||||
}));
|
||||
}
|
||||
}
|
||||
exports.Socket = Socket;
|
||||
if (typeof window !== "undefined") {
|
||||
window.Webcast = {
|
||||
version: "1.0.0",
|
||||
Socket,
|
||||
};
|
||||
}
|
Loading…
Reference in New Issue