WIP
This commit is contained in:
parent
9738be7302
commit
000b8323e1
4 changed files with 139 additions and 6 deletions
113
src/broadcast/rtc_streamer.ts
Normal file
113
src/broadcast/rtc_streamer.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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());
|
||||
};
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue