Files
podsteadr/frontend/src/lib/whip.ts
T

63 lines
1.8 KiB
TypeScript
Raw Normal View History

// Minimal WHIP (WebRTC-HTTP Ingestion Protocol) publisher for MediaMTX.
export interface WhipSession {
pc: RTCPeerConnection;
stop: () => Promise<void>;
}
export async function publishWhip(
whipUrl: string,
bearer: string,
stream: MediaStream,
): Promise<WhipSession> {
const pc = new RTCPeerConnection();
for (const track of stream.getTracks()) {
pc.addTransceiver(track, { direction: 'sendonly' });
}
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
await waitIceComplete(pc);
const res = await fetch(whipUrl, {
method: 'POST',
headers: {
'content-type': 'application/sdp',
authorization: `Bearer ${bearer}`,
},
body: pc.localDescription!.sdp,
});
if (!res.ok) {
pc.close();
throw new Error(`WHIP publish failed: ${res.status} ${await res.text().catch(() => '')}`);
}
const location = res.headers.get('location');
const answer = await res.text();
await pc.setRemoteDescription({ type: 'answer', sdp: answer });
return {
pc,
stop: async () => {
pc.close();
for (const track of stream.getTracks()) track.stop();
if (location) {
const url = new URL(location, whipUrl).toString();
await fetch(url, { method: 'DELETE', headers: { authorization: `Bearer ${bearer}` } }).catch(() => {});
}
},
};
}
function waitIceComplete(pc: RTCPeerConnection): Promise<void> {
if (pc.iceGatheringState === 'complete') return Promise.resolve();
return new Promise((resolve) => {
const timeout = setTimeout(resolve, 2000); // trickle fallback: send what we have
pc.addEventListener('icegatheringstatechange', () => {
if (pc.iceGatheringState === 'complete') {
clearTimeout(timeout);
resolve();
}
});
});
}