WIP majority of refactor done.

This commit is contained in:
Buster Neece 2022-12-31 14:07:32 -06:00
parent 48014dab6b
commit b498288965
No known key found for this signature in database
GPG Key ID: F1D2E64A0005E80E
15 changed files with 513 additions and 1127 deletions

View File

@ -1,2 +1,2 @@
vue/components/Public/WebDJ/*
vue/vendor/chartjs-colorschemes/*
vue/vendor/webcast/*

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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;

View File

@ -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);
}
}
};

View File

@ -0,0 +1,8 @@
import {createGlobalState} from "@vueuse/core";
import {ref} from "vue";
export function useMixerValue() {
return createGlobalState(
() => ref(0.5)
);
}

View File

@ -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
};
}

View File

@ -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,
};
}

View File

@ -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

View File

@ -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;
};
}
}

View File

@ -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,
};
}