This commit is contained in:
Marks Polakovs 2020-03-17 22:11:49 +00:00
parent 2ea2002951
commit 9c0cde9d6b
8 changed files with 567 additions and 10 deletions

View file

@ -24,7 +24,8 @@
"react-redux": "^7.1.3",
"react-scripts": "3.2.0",
"redux": "^4.0.4",
"typescript": "3.7.2"
"typescript": "3.7.2",
"webcast.js": "ssh://git@github.com/UniversityRadioYork/webcast.js"
},
"scripts": {
"start": "react-scripts start",
@ -46,5 +47,8 @@
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"prettier": "^1.19.1"
}
}

View file

@ -39,5 +39,8 @@
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
<script src="//cdn.rawgit.com/toots/shine/master/js/dist/libshine.js"></script>
<script src="//cdn.rawgit.com/webcast/libsamplerate.js/master/dist/libsamplerate.js"></script>
<script src="//cdn.rawgit.com/webcast/taglib.js/master/dist/taglib.js"></script>
</body>
</html>

View file

@ -85,3 +85,15 @@ html, body, #root {
.react-contextmenu-item--disabled {
cursor: not-allowed;
}
.sp-state-playing {
background-color: #2cdb2c;
}
.sp-state-paused {
background-color: #dbbb2c;
}
.sp-state-stopped {
background-color: #db2c2c;
}

447
src/lib/webcast.js Normal file
View file

@ -0,0 +1,447 @@
// Generated by CoffeeScript 1.11.1
/* eslint-disable */
(function() {
var AudioContext, Webcast;
Webcast = {
Encoder: {}
};
if (typeof window !== "undefined") {
window.Webcast = Webcast;
}
if (typeof self !== "undefined") {
self.Webcast = Webcast;
}
Webcast.Encoder.Asynchronous = (function() {
function Asynchronous(arg) {
var blob, j, len1, script, scripts;
this.encoder = arg.encoder, scripts = arg.scripts;
this.mime = this.encoder.mime;
this.info = this.encoder.info;
this.channels = this.encoder.channels;
this.pending = [];
this.scripts = [];
for (j = 0, len1 = scripts.length; j < len1; j++) {
script = scripts[j];
this.scripts.push("'" + script + "'");
}
script = "var window;\nimportScripts(" + (this.scripts.join()) + ");\nvar encoder = " + (this.encoder.toString()) + ";\nself.onmessage = function (e) {\n var type = e.data.type;\n var data = e.data.data;\n if (type === \"buffer\") {\n encoder.encode(data, function (encoded) {\n postMessage(encoded);\n });\n return;\n }\n if (type === \"close\") {\n encoder.close(function (buffer) {\n postMessage({close:true, buffer:buffer});\n self.close();\n });\n return;\n }\n};";
blob = new Blob([script], {
type: "text/javascript"
});
this.worker = new Worker(URL.createObjectURL(blob));
this.worker.onmessage = (function(_this) {
return function(arg1) {
var data;
data = arg1.data;
return _this.pending.push(data);
};
})(this);
}
Asynchronous.prototype.toString = function() {
return "(new Webcast.Encoder.Asynchronous({\n encoder: " + (this.encoder.toString()) + ",\n scripts: [" + (this.scripts.join()) + "]\n}))";
};
Asynchronous.prototype.close = function(fn) {
this.worker.onmessage = (function(_this) {
return function(arg) {
var chunk, data, j, k, len, len1, len2, offset, ref, ref1, ret;
data = arg.data;
if (!data.close) {
_this.pending.push(data);
return;
}
_this.pending.push(data.buffer);
len = 0;
ref = _this.pending;
for (j = 0, len1 = ref.length; j < len1; j++) {
chunk = ref[j];
len += chunk.length;
}
ret = new Uint8Array(len);
offset = 0;
ref1 = _this.pending;
for (k = 0, len2 = ref1.length; k < len2; k++) {
chunk = ref1[k];
ret.set(chunk, offset);
offset += chunk.length;
}
return fn(ret);
};
})(this);
return this.worker.postMessage({
type: "close"
});
};
Asynchronous.prototype.encode = function(buffer, fn) {
this.worker.postMessage({
type: "buffer",
data: buffer
});
return fn(this.pending.shift());
};
return Asynchronous;
})();
if (typeof window !== "undefined") {
AudioContext = window.AudioContext || window.webkitAudioContext;
AudioContext.prototype.createWebcastSource = function(bufferSize, channels, passThrough) {
var context, node, options;
context = this;
node = context.createScriptProcessor(bufferSize, channels, channels);
passThrough || (passThrough = false);
options = {
recorderSource: null,
encoder: null,
socket: null,
passThrough: passThrough || false
};
node.onaudioprocess = function(buf) {
var audio, channel, channelData, j, ref, ref1;
audio = [];
for (channel = j = 0, ref = buf.inputBuffer.numberOfChannels - 1; 0 <= ref ? j <= ref : j >= ref; channel = 0 <= ref ? ++j : --j) {
channelData = buf.inputBuffer.getChannelData(channel);
audio[channel] = channelData;
if (options.passThrough) {
buf.outputBuffer.getChannelData(channel).set(channelData);
} else {
buf.outputBuffer.getChannelData(channel).set(new Float32Array(channelData.length));
}
}
return (ref1 = options.encoder) != null ? typeof ref1.encode === "function" ? ref1.encode(audio, function(data) {
var ref2;
if (data != null) {
return (ref2 = options.socket) != null ? ref2.sendData(data) : void 0;
}
}) : void 0 : void 0;
};
node.setPassThrough = function(b) {
return options.passThrough = b;
};
node.connectSocket = function(encoder, url) {
if (encoder instanceof Webcast.Recorder) {
options.recorderSource = context.createMediaStreamDestination();
node.connect(options.recorderSource);
encoder.start(options.recoderSource.stream, function(data) {
var ref;
if (data != null) {
return (ref = options.socket) != null ? ref.sendData(data) : void 0;
}
});
}
options.encoder = encoder;
return options.socket = new Webcast.Socket({
url: url,
mime: options.encoder.mime,
info: options.encoder.info
});
};
node.close = function(cb) {
var fn, ref, ref1;
if ((ref = options.recorderSource) != null) {
ref.disconnect();
}
options.recorderSource = null;
fn = function() {
var ref1;
if ((ref1 = options.socket) != null) {
ref1.close();
}
options.socket = options.encoder = null;
return typeof cb === "function" ? cb() : void 0;
};
if (((ref1 = options.encoder) != null ? ref1.close : void 0) == null) {
return fn();
}
return options.encoder.close(function(data) {
var ref2;
if ((ref2 = options.socket) != null) {
ref2.sendData(data);
}
return fn();
});
};
node.getSocket = function() {
return options.socket;
};
node.sendMetadata = (function(_this) {
return function(metadata) {
var ref;
return (ref = options.socket) != null ? ref.sendMetadata(metadata) : void 0;
};
})(this);
node.isOpen = function() {
return options != null ? options.socket.isOpen() : void 0;
};
return node;
};
}
Webcast.Encoder.Mp3 = (function() {
Mp3.prototype.mime = "audio/mpeg";
function Mp3(arg) {
this.samplerate = arg.samplerate, this.bitrate = arg.bitrate, this.channels = arg.channels;
this.shine = new Shine({
samplerate: this.samplerate,
bitrate: this.bitrate,
channels: this.channels,
mode: this.channels === 1 ? Shine.MONO : Shine.JOINT_STEREO
});
this.info = {
audio: {
channels: this.channels,
samplerate: this.samplerate,
bitrate: this.bitrate,
encoder: "libshine"
}
};
this;
}
Mp3.prototype.toString = function() {
return "(new Webcast.Encoder.Mp3({\n bitrate: " + this.bitrate + ",\n channels: " + this.channels + ",\n samplerate: " + this.samplerate + "\n }))";
};
Mp3.prototype.close = function(data, fn) {
var flushed, rem;
rem = new Uint8Array;
if (fn != null) {
if ((data != null ? data.length : void 0) > 0) {
rem = this.shine.encode(data);
}
} else {
fn = data;
}
flushed = this.shine.close();
data = new Uint8Array(rem.length + flushed.length);
data.set(rem);
data.set(flushed, rem.length);
return fn(data);
};
Mp3.prototype.encode = function(data, fn) {
data = data.slice(0, this.channels);
return fn(this.shine.encode(data));
};
return Mp3;
})();
Webcast.Encoder.Raw = (function() {
function Raw(arg) {
this.channels = arg.channels, this.samplerate = arg.samplerate;
this.mime = "audio/x-raw,format=S8,channels=" + this.channels + ",layout=interleaved,rate=" + this.samplerate;
this.info = {
audio: {
channels: this.channels,
samplerate: this.samplerate,
encoder: "RAW u8 encoder"
}
};
}
Raw.prototype.toString = function() {
return "(new Webcast.Encoder.Raw({\n channels: " + this.channels + ",\n samplerate: " + this.samplerate + "\n }))";
};
Raw.prototype.doEncode = function(data) {
var buf, chan, channels, i, j, k, ref, ref1, samples;
channels = data.length;
samples = data[0].length;
buf = new Int8Array(channels * samples);
for (chan = j = 0, ref = channels - 1; 0 <= ref ? j <= ref : j >= ref; chan = 0 <= ref ? ++j : --j) {
for (i = k = 0, ref1 = samples - 1; 0 <= ref1 ? k <= ref1 : k >= ref1; i = 0 <= ref1 ? ++k : --k) {
buf[channels * i + chan] = data[chan][i] * 127;
}
}
return buf;
};
Raw.prototype.close = function(data, fn) {
var ret;
ret = new Uint8Array;
if (fn != null) {
if ((data != null ? data.count : void 0) > 0) {
ret = this.doEncode(data);
}
} else {
fn = data;
}
return fn(ret);
};
Raw.prototype.encode = function(data, fn) {
return fn(this.doEncode(data));
};
return Raw;
})();
Webcast.Recorder = (function() {
Recorder.prototype.mime = "audio/ogg";
function Recorder(arg) {
this.samplerate = arg.samplerate, this.bitrate = arg.bitrate, this.channels = arg.channels;
this.info = {
audio: {
channels: this.channels,
samplerate: this.samplerate,
bitrate: this.bitrate,
encoder: "MediaRecorder"
}
};
}
Recorder.prototype.start = function(stream, cb) {
var recorder;
recorder = new MediaRecorder(stream);
return recorder.ondataavailable = (function(_this) {
return function(e) {
var blob;
if (recorder.state === "recording") {
blob = new Blob([e.data], _this.mime);
return cb(blob);
}
};
})(this);
};
return Recorder;
})();
Webcast.Encoder.Resample = (function() {
function Resample(arg) {
var i, j, ref;
this.encoder = arg.encoder, this.samplerate = arg.samplerate, this.type = arg.type;
this.mime = this.encoder.mime;
this.info = this.encoder.info;
this.channels = this.encoder.channels;
this.ratio = parseFloat(this.encoder.samplerate) / parseFloat(this.samplerate);
this.type = this.type || Samplerate.FASTEST;
this.resamplers = [];
this.remaining = [];
for (i = j = 0, ref = this.channels - 1; 0 <= ref ? j <= ref : j >= ref; i = 0 <= ref ? ++j : --j) {
this.resamplers[i] = new Samplerate({
type: this.type
});
this.remaining[i] = new Float32Array;
}
}
Resample.prototype.toString = function() {
return "(new Webcast.Encoder.Resample({\n encoder: " + (this.encoder.toString()) + ",\n samplerate: " + this.samplerate + ",\n type: " + this.type + "\n }))";
};
Resample.prototype.close = function(fn) {
var data, i, j, ref;
for (i = j = 0, ref = this.remaining.length - 1; 0 <= ref ? j <= ref : j >= ref; i = 0 <= ref ? ++j : --j) {
data = this.resamplers[i].process({
data: this.remaining[i],
ratio: this.ratio,
last: true
}).data;
}
return this.encoder.close(data, fn);
};
Resample.prototype.concat = function(a, b) {
var ret;
if (typeof b === "undefined") {
return a;
}
ret = new Float32Array(a.length + b.length);
ret.set(a);
ret.subarray(a.length).set(b);
return ret;
};
Resample.prototype.encode = function(buffer, fn) {
var data, i, j, ref, ref1, used;
for (i = j = 0, ref = this.channels - 1; 0 <= ref ? j <= ref : j >= ref; i = 0 <= ref ? ++j : --j) {
buffer[i] = this.concat(this.remaining[i], buffer[i]);
ref1 = this.resamplers[i].process({
data: buffer[i],
ratio: this.ratio
}), data = ref1.data, used = ref1.used;
this.remaining[i] = buffer[i].subarray(used);
buffer[i] = data;
}
return this.encoder.encode(buffer, fn);
};
return Resample;
})();
Webcast.Socket = function(arg) {
var hello, info, key, mime, parser, password, send, socket, url, user, value;
url = arg.url, mime = arg.mime, info = arg.info;
parser = document.createElement("a");
parser.href = url;
user = parser.username;
password = parser.password;
parser.username = parser.password = "";
url = parser.href;
socket = new WebSocket(url, "webcast");
socket.mime = mime;
socket.info = info;
hello = {
mime: mime
};
if ((user != null) && user !== "") {
hello.user = socket.user = user;
}
if ((password != null) && password !== "") {
hello.password = socket.password = password;
}
for (key in info) {
value = info[key];
hello[key] = value;
}
send = socket.send;
socket.send = null;
socket.addEventListener("open", function() {
return send.call(socket, JSON.stringify({
type: "hello",
data: hello
}));
});
socket.sendData = function(data) {
if (!socket.isOpen()) {
return;
}
if (!((data != null ? data.length : void 0) > 0)) {
return;
}
if (!(data instanceof ArrayBuffer)) {
data = data.buffer.slice(data.byteOffset, data.length * data.BYTES_PER_ELEMENT);
}
return send.call(socket, data);
};
socket.sendMetadata = function(metadata) {
if (!socket.isOpen()) {
return;
}
return send.call(socket, JSON.stringify({
type: "metadata",
data: metadata
}));
};
socket.isOpen = function() {
return socket.readyState === WebSocket.OPEN;
};
return socket;
};
}).call(this);

View file

@ -83,9 +83,18 @@ function Player({ id }: { id: number }) {
<div>
{playerState.loadedItem !== null && (<div>{playerState.loadedItem.title}</div>)}
{playerState.loading && <b>LOADING</b>}
<button onClick={() => dispatch(PlayerState.play(id))}>p</button>
<button onClick={() => dispatch(PlayerState.pause(id))}>u</button>
<button onClick={() => dispatch(PlayerState.stop(id))}>s</button>
<button
onClick={() => dispatch(PlayerState.play(id))}
className={playerState.state === "playing" ? "sp-state-playing" : ""}
>p</button>
<button
onClick={() => dispatch(PlayerState.pause(id))}
className={playerState.state === "paused" ? "sp-state-paused" : ""}
>a</button>
<button
onClick={() => dispatch(PlayerState.stop(id))}
className={playerState.state === "stopped" ? "sp-state-stopped" : ""}
>s</button>
</div>
);
}

View file

@ -1,16 +1,26 @@
import "../../lib/webcast";
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { PlanItem } from "../state";
import { Track, MYRADIO_NON_API_BASE } from "../../api";
import { AppThunk } from "../../store";
/// <reference path="webcast.d.ts" />
const audioContext = new AudioContext();
const playerSources: MediaElementAudioSourceNode[] = [];
const playerGains: GainNode[] = [];
const destination = audioContext.createMediaStreamDestination();
// TODO
// const destination = audioContext.createWebcastSource(4096, 2);
const destination = audioContext.createDynamicsCompressor();
destination.connect(audioContext.destination);
type PlayerStateEnum = "playing" | "paused" | "stopped";
interface SinglePlayerState {
loadedItem: PlanItem | Track | null
loading: boolean;
state: PlayerStateEnum;
}
interface PlayerState {
@ -22,13 +32,16 @@ const playerState = createSlice({
initialState: {
players: [{
loadedItem: null,
loading: false
loading: false,
state: "stopped"
}, {
loadedItem: null,
loading: false
loading: false,
state: "stopped"
}, {
loadedItem: null,
loading: false
loading: false,
state: "stopped"
}]
} as PlayerState,
reducers: {
@ -38,6 +51,9 @@ const playerState = createSlice({
},
itemLoadComplete(state, action: PayloadAction<{ player: number}>) {
state.players[action.payload.player].loading = false;
},
setPlayerState(state, action: PayloadAction<{ player: number, state: PlayerStateEnum }>) {
state.players[action.payload.player].state = action.payload.state;
}
}
});
@ -69,25 +85,29 @@ export const load = (player: number, item: PlanItem | Track): AppThunk => dispat
const sauce = audioContext.createMediaElementSource(el);
const gain = audioContext.createGain();
sauce.connect(gain);
gain.connect(audioContext.destination);
gain.connect(destination);
console.log("Connected to", destination);
playerSources[player] = sauce;
playerGains[player] = gain;
}
export const play = (player: number): AppThunk => dispatch => {
playerSources[player].mediaElement.play();
dispatch(playerState.actions.setPlayerState({ player, state: "playing" }));
};
export const pause = (player: number): AppThunk => dispatch => {
if (playerSources[player].mediaElement.paused) {
playerSources[player].mediaElement.play();
dispatch(playerState.actions.setPlayerState({ player, state: "playing" }));
} else {
playerSources[player].mediaElement.pause();
dispatch(playerState.actions.setPlayerState({ player, state: "paused" }));
}
};
export const stop = (player: number): AppThunk => dispatch => {
playerSources[player].mediaElement.pause();
playerSources[player].mediaElement.currentTime = 0;
dispatch(playerState.actions.setPlayerState({ player, state: "stopped" }));
};

53
src/showplanner/player/webcast.d.ts vendored Normal file
View file

@ -0,0 +1,53 @@
class WebcastEncoder {
doEncode(data: any): any;
}
interface WebcastAudioSourceNode extends AudioNode {
setPassThrough(val: boolean): boolean;
connectSocket(encoder: WebcastEncoder, url: string): Webcast.Socket;
close(cb: any): any; // TODO
getSocket(): Webcast.Socket;
sendMetadata(meta: any): any;
isOpen(): boolean | undefined;
}
declare interface AudioContext {
createWebcastSource(bufferSize: number, channels: number, passThrough?: boolean): WebcastAudioSourceNode;
}
declare namespace Webcast {
declare namespace Encoder {
class Asynchronous extends WebcastEncoder {
constructor(options: {
encoder: WebcastEncoder,
scripts: string[]
})
}
class Resample extends WebcastEncoder {
constructor(options: {
encoder: WebcastEncoder,
samplerate: number
})
}
class Mp3 extends WebcastEncoder {
constructor(options: {
samplerate: number,
bitrate: number,
channels: number,
})
}
class Raw extends WebcastEncoder {
constructor(options: {
samplerate: number,
channels: number,
})
}
}
class Socket extends WebSocket {
// TODO
}
}

View file

@ -7896,6 +7896,11 @@ prepend-http@^1.0.0:
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc"
integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=
prettier@^1.19.1:
version "1.19.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb"
integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==
pretty-bytes@^5.1.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.3.0.tgz#f2849e27db79fb4d6cfe24764fc4134f165989f2"
@ -9997,6 +10002,10 @@ wbuf@^1.1.0, wbuf@^1.7.3:
dependencies:
minimalistic-assert "^1.0.0"
"webcast.js@ssh://git@github.com/UniversityRadioYork/webcast.js":
version "0.0.0"
resolved "ssh://git@github.com/UniversityRadioYork/webcast.js#1ebc76dbe05033208a107feae986c0edb38e8261"
webidl-conversions@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"