-
-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #345 from CodeCrowCorp/dev
Dev
- Loading branch information
Showing
4 changed files
with
298 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,2 @@ | ||
fixes: | ||
- "::/mage-website" | ||
- "/home/runner/work/mage-website/::" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
import negotiateConnectionWithClientOffer from '$lib/negotiateConnectionWithClientOffer' | ||
|
||
/** | ||
* Example implementation of a client that uses WHEP to playback video over WebRTC | ||
* | ||
* https://www.ietf.org/id/draft-murillo-whep-00.html | ||
*/ | ||
export default class WHEPClient { | ||
private peerConnection: RTCPeerConnection | ||
private stream: MediaStream | ||
|
||
constructor(private endpoint: string, private videoElement: HTMLVideoElement) { | ||
this.stream = new MediaStream() | ||
|
||
/** | ||
* Create a new WebRTC connection, using public STUN servers with ICE, | ||
* allowing the client to disover its own IP address. | ||
* https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Protocols#ice | ||
*/ | ||
this.peerConnection = new RTCPeerConnection({ | ||
iceServers: [ | ||
{ | ||
urls: 'stun:stun.cloudflare.com:3478' | ||
} | ||
], | ||
bundlePolicy: 'max-bundle' | ||
}) | ||
|
||
/** https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/addTransceiver */ | ||
this.peerConnection.addTransceiver('video', { | ||
direction: 'recvonly' | ||
}) | ||
this.peerConnection.addTransceiver('audio', { | ||
direction: 'recvonly' | ||
}) | ||
|
||
/** | ||
* When new tracks are received in the connection, store local references, | ||
* so that they can be added to a MediaStream, and to the <video> element. | ||
* | ||
* https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/track_event | ||
*/ | ||
this.peerConnection.ontrack = (event) => { | ||
const track = event.track | ||
const currentTracks = this.stream.getTracks() | ||
const streamAlreadyHasVideoTrack = currentTracks.some((track) => track.kind === 'video') | ||
const streamAlreadyHasAudioTrack = currentTracks.some((track) => track.kind === 'audio') | ||
switch (track.kind) { | ||
case 'video': | ||
if (streamAlreadyHasVideoTrack) { | ||
break | ||
} | ||
this.stream.addTrack(track) | ||
break | ||
case 'audio': | ||
if (streamAlreadyHasAudioTrack) { | ||
break | ||
} | ||
this.stream.addTrack(track) | ||
break | ||
default: | ||
console.log('got unknown track ' + track) | ||
} | ||
} | ||
|
||
this.peerConnection.addEventListener('connectionstatechange', (ev) => { | ||
if (this.peerConnection.connectionState !== 'connected') { | ||
return | ||
} | ||
if (!this.videoElement.srcObject) { | ||
this.videoElement.srcObject = this.stream | ||
} | ||
}) | ||
|
||
this.peerConnection.addEventListener('negotiationneeded', (ev) => { | ||
negotiateConnectionWithClientOffer(this.peerConnection, this.endpoint) | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
import negotiateConnectionWithClientOffer from '$lib/negotiateConnectionWithClientOffer' | ||
|
||
/** | ||
* Example implementation of a client that uses WHIP to broadcast video over WebRTC | ||
* | ||
* https://www.ietf.org/archive/id/draft-ietf-wish-whip-01.html | ||
*/ | ||
export default class WHIPClient { | ||
private peerConnection: RTCPeerConnection | ||
private localStream?: MediaStream | ||
|
||
constructor(private endpoint: string, private videoElement: HTMLVideoElement, trackType: string) { | ||
/** | ||
* Create a new WebRTC connection, using public STUN servers with ICE, | ||
* allowing the client to disover its own IP address. | ||
* https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Protocols#ice | ||
*/ | ||
this.peerConnection = new RTCPeerConnection({ | ||
iceServers: [ | ||
{ | ||
urls: 'stun:stun.cloudflare.com:3478' | ||
} | ||
], | ||
bundlePolicy: 'max-bundle' | ||
}) | ||
|
||
/** | ||
* Listen for negotiationneeded events, and use WHIP as the signaling protocol to establish a connection | ||
* | ||
* https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/negotiationneeded_event | ||
* https://www.ietf.org/archive/id/draft-ietf-wish-whip-01.html | ||
*/ | ||
this.peerConnection.addEventListener('negotiationneeded', async (ev) => { | ||
console.log('Connection negotiation starting') | ||
await negotiateConnectionWithClientOffer(this.peerConnection, this.endpoint) | ||
console.log('Connection negotiation ended') | ||
}) | ||
|
||
/** | ||
* While the connection is being initialized, | ||
* connect the video stream to the provided <video> element. | ||
*/ | ||
this.accessLocalMediaSources(trackType) | ||
.then((stream) => { | ||
this.localStream = stream | ||
videoElement.srcObject = stream as MediaProvider | ||
}) | ||
.catch(console.error) | ||
} | ||
|
||
/** | ||
* Ask for camera and microphone permissions and | ||
* add video and audio tracks to the peerConnection. | ||
* | ||
* https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia | ||
*/ | ||
private async accessLocalMediaSources(trackType: string) { | ||
if (trackType === 'screen') { | ||
return navigator.mediaDevices.getDisplayMedia({ video: true, audio: true }).then((stream) => { | ||
stream.getTracks().forEach((track) => { | ||
const transceiver = this.peerConnection.addTransceiver(track, { | ||
/** WHIP is only for sending streaming media */ | ||
direction: 'sendonly' | ||
}) | ||
if (track.kind == 'video' && transceiver.sender.track) { | ||
transceiver.sender.track.applyConstraints({ | ||
width: 1280, | ||
height: 720 | ||
}) | ||
} | ||
}) | ||
return stream | ||
}) | ||
} else if (trackType === 'webcam') { | ||
return navigator.mediaDevices.getUserMedia({ video: true, audio: true }).then((stream) => { | ||
stream.getTracks().forEach((track) => { | ||
const transceiver = this.peerConnection.addTransceiver(track, { | ||
/** WHIP is only for sending streaming media */ | ||
direction: 'sendonly' | ||
}) | ||
if (track.kind == 'video' && transceiver.sender.track) { | ||
transceiver.sender.track.applyConstraints({ | ||
width: 1280, | ||
height: 720 | ||
}) | ||
} | ||
}) | ||
return stream | ||
}) | ||
} else if (trackType === 'audio') { | ||
return navigator.mediaDevices | ||
.getUserMedia({ | ||
video: false, | ||
audio: { | ||
echoCancellation: true, | ||
noiseSuppression: true, | ||
deviceId: 'default' | ||
} | ||
}) | ||
.then((stream) => { | ||
stream.getTracks().forEach((track) => { | ||
const transceiver = this.peerConnection.addTransceiver(track, { | ||
/** WHIP is only for sending streaming media */ | ||
direction: 'sendonly' | ||
}) | ||
if (track.kind == 'video' && transceiver.sender.track) { | ||
transceiver.sender.track.applyConstraints({ | ||
width: 1280, | ||
height: 720 | ||
}) | ||
} | ||
}) | ||
return stream | ||
}) | ||
} | ||
} | ||
|
||
/** | ||
* Terminate the streaming session | ||
* 1. Notify the WHIP server by sending a DELETE request | ||
* 2. Close the WebRTC connection | ||
* 3. Stop using the local camera and microphone | ||
* | ||
* Note that once you call this method, this instance of this WHIPClient cannot be reused. | ||
*/ | ||
public async disconnectStream() { | ||
const response = await fetch(this.endpoint, { | ||
method: 'DELETE', | ||
mode: 'cors' | ||
}) | ||
this.peerConnection.close() | ||
this.localStream?.getTracks().forEach((track) => track.stop()) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
/** | ||
* Performs the actual SDP exchange. | ||
* | ||
* 1. Constructs the client's SDP offer | ||
* 2. Sends the SDP offer to the server, | ||
* 3. Awaits the server's offer. | ||
* | ||
* SDP describes what kind of media we can send and how the server and client communicate. | ||
* | ||
* https://developer.mozilla.org/en-US/docs/Glossary/SDP | ||
* https://www.ietf.org/archive/id/draft-ietf-wish-whip-01.html#name-protocol-operation | ||
*/ | ||
export default async function negotiateConnectionWithClientOffer( | ||
peerConnection: RTCPeerConnection, | ||
endpoint: string | ||
) { | ||
/** https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/createOffer */ | ||
const offer = await peerConnection.createOffer() | ||
/** https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/setLocalDescription */ | ||
await peerConnection.setLocalDescription(offer) | ||
|
||
/** Wait for ICE gathering to complete */ | ||
const ofr = await waitToCompleteICEGathering(peerConnection) | ||
if (!ofr) { | ||
throw Error('failed to gather ICE candidates for offer') | ||
} | ||
|
||
/** | ||
* As long as the connection is open, attempt to... | ||
*/ | ||
while (peerConnection.connectionState !== 'closed') { | ||
/** | ||
* This response contains the server's SDP offer. | ||
* This specifies how the client should communicate, | ||
* and what kind of media client and server have negotiated to exchange. | ||
*/ | ||
const response = await postSDPOffer(endpoint, ofr.sdp) | ||
if (response.status === 201) { | ||
const answerSDP = await response.text() | ||
await peerConnection.setRemoteDescription( | ||
new RTCSessionDescription({ type: 'answer', sdp: answerSDP }) | ||
) | ||
return response.headers.get('Location') | ||
} else if (response.status === 405) { | ||
console.log('Remember to update the URL passed into the WHIP or WHEP client') | ||
} else { | ||
const errorMessage = await response.text() | ||
console.error(errorMessage) | ||
} | ||
|
||
/** Limit reconnection attempts to at-most once every 5 seconds */ | ||
await new Promise((r) => setTimeout(r, 5000)) | ||
} | ||
} | ||
|
||
async function postSDPOffer(endpoint: string, data: string) { | ||
return await fetch(endpoint, { | ||
method: 'POST', | ||
mode: 'cors', | ||
headers: { | ||
'content-type': 'application/sdp' | ||
}, | ||
body: data | ||
}) | ||
} | ||
|
||
/** | ||
* Receives an RTCPeerConnection and waits until | ||
* the connection is initialized or a timeout passes. | ||
* | ||
* https://www.ietf.org/archive/id/draft-ietf-wish-whip-01.html#section-4.1 | ||
* https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/iceGatheringState | ||
* https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/icegatheringstatechange_event | ||
*/ | ||
async function waitToCompleteICEGathering(peerConnection: RTCPeerConnection) { | ||
return new Promise<RTCSessionDescription | null>((resolve) => { | ||
/** Wait at most 1 second for ICE gathering. */ | ||
setTimeout(function () { | ||
resolve(peerConnection.localDescription) | ||
}, 1000) | ||
peerConnection.onicegatheringstatechange = (ev) => | ||
peerConnection.iceGatheringState === 'complete' && resolve(peerConnection.localDescription) | ||
}) | ||
} |