This commit is contained in:
Marks Polakovs 2020-03-26 15:39:25 +01:00
parent 9738be7302
commit 000b8323e1
4 changed files with 139 additions and 6 deletions

View file

@ -0,0 +1,113 @@
type StreamerState = "HELLO" | "OFFER" | "ANSWER" | "CONNECTED";
export type ConnectionStateEnum = "NOT_CONNECTED" | "CONNECTING" | "CONNECTED" | "CONNECTION_LOST";
type ConnectionStateListener = (val: ConnectionStateEnum) => any;
export class WebRTCStreamer {
pc: RTCPeerConnection;
ws: WebSocket;
state: StreamerState = "HELLO";
csListeners: ConnectionStateListener[] = [];
constructor(stream: MediaStream) {
this.pc = new RTCPeerConnection({});
this.pc.onconnectionstatechange = e => {
console.log("Connection state change: " + this.pc.connectionState);
this.csListeners.forEach(l => l(this.mapStateToConnectionState()));
};
console.log("Stream tracks", stream.getAudioTracks());
stream.getAudioTracks().forEach(track => this.pc.addTrack(track));
this.ws = new WebSocket("ws://localhost:8079/stream"); // TODO
this.ws.onopen = e => {
console.log("WS open");
this.csListeners.forEach(l => l(this.mapStateToConnectionState()));
};
this.ws.onclose = e => {
console.log("WS close");
this.csListeners.forEach(l => l(this.mapStateToConnectionState()));
};
this.ws.addEventListener("message", this.onMessage.bind(this));
}
async onMessage(evt: MessageEvent) {
const data = JSON.parse(evt.data);
switch (data.kind) {
case "HELLO":
console.log("WS HELLO, our client ID is " + data.connectionId);
if (this.state !== "HELLO") {
this.ws.close();
}
const offer = await this.pc.createOffer();
// TODO do some fun SDP fuckery to get quality
await this.pc.setLocalDescription(offer);
await this.waitForIceCandidates();
this.ws.send(
JSON.stringify({
kind: "OFFER",
type: this.pc.localDescription!.type,
sdp: this.pc.localDescription!.sdp
})
);
this.state = "OFFER";
break;
case "ANSWER":
const answer = new RTCSessionDescription({
type: data.type,
sdp: data.sdp
});
await this.pc.setRemoteDescription(answer);
this.state = "ANSWER";
break;
}
}
// TODO: supporting trickle ICE would be nICE
waitForIceCandidates() {
return new Promise(resolve => {
if (this.pc.iceGatheringState === "complete") {
resolve();
} else {
const check = () => {
if (this.pc.iceGatheringState === "complete") {
this.pc.removeEventListener(
"icegatheringstatechange",
check
);
resolve();
}
};
this.pc.addEventListener("icegatheringstatechange", check);
}
});
}
close() {
this.ws.close();
this.pc.close();
}
mapStateToConnectionState(): ConnectionStateEnum {
switch (this.pc.connectionState) {
case "connected": return "CONNECTED";
case "connecting": return "CONNECTING";
case "disconnected": return "CONNECTION_LOST";
case "failed": return "CONNECTION_LOST";
default:
switch (this.ws.readyState) {
case 1: return "CONNECTING";
case 2: case 3: return "CONNECTION_LOST";
case 0: return "NOT_CONNECTED";
default: throw new Error();
}
}
}
addConnectionStateListener(listener: ConnectionStateListener) {
this.csListeners.push(listener);
listener(this.mapStateToConnectionState());
return () => {
this.csListeners.splice(this.csListeners.indexOf(listener), 1);
}
}
}

View file

@ -8,22 +8,30 @@ import {
} from "@reduxjs/toolkit";
import { AppThunk } from "../store";
import { Track, myradioApiRequest } from "../api";
import { WebRTCStreamer, ConnectionStateEnum } from "./rtc_streamer";
import { destination } from "../mixer/state";
let streamer: WebRTCStreamer | null = null;
interface BroadcastState {
tracklisting: boolean;
connectionState: ConnectionStateEnum;
}
const broadcastState = createSlice({
name: "Broadcast",
initialState: {
tracklisting: false
tracklisting: false,
connectionState: "NOT_CONNECTED"
} as BroadcastState,
reducers: {
setTracklisting(
state
) {
state.tracklisting = !state.tracklisting;
},
setConnectionState(state, action: PayloadAction<ConnectionStateEnum>) {
state.connectionState = action.payload;
}
}
});
@ -50,3 +58,10 @@ export const tracklistEnd = (trackid: number): AppThunk => (dispatch, getState)
myradioApiRequest("/TracklistItem", "POST", { trackid: trackid, source: 'w', state: 'c' });
}
};
export const connect = (): AppThunk => dispatch => {
streamer = new WebRTCStreamer(destination.stream);
streamer.addConnectionStateListener(state => {
dispatch(broadcastState.actions.setConnectionState(state));
});
};

View file

@ -29,10 +29,12 @@ let micSource: MediaStreamAudioSourceNode | null = null;
let micGain: GainNode | null = null;
let micCompressor: DynamicsCompressorNode | null = null;
// TODO
// const destination = audioContext.createWebcastSource(4096, 2);
const destination = audioContext.createDynamicsCompressor();
destination.connect(audioContext.destination);
const finalCompressor = audioContext.createDynamicsCompressor();
finalCompressor.connect(audioContext.destination);
export const destination = audioContext.createMediaStreamDestination();
console.log("final destination", destination);
finalCompressor.connect(destination);
type PlayerStateEnum = "playing" | "paused" | "stopped";
type PlayerRepeatEnum = "none" | "one" | "all";
@ -548,7 +550,7 @@ export const openMicrophone = (): AppThunk => async (dispatch, getState) => {
micSource
.connect(micGain)
.connect(micCompressor)
.connect(destination);
.connect(finalCompressor);
dispatch(mixerState.actions.micOpen());
};

View file

@ -364,6 +364,9 @@ function NavBar() {
<li className="nav-item nav-link">
<button className="" onClick={() => dispatch(BroadcastState.toggleTracklisting())}>{broadcastState.tracklisting ? "Tracklisting!" : "Not Tracklisting"} </button>
</li>
<li className="nav-item nav-link">
<button className="" onClick={() => dispatch(BroadcastState.connect())}>{broadcastState.connectionState}</button>
</li>
<li className="nav-item">
<a
className="nav-link"