"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = useWebRTC;
exports.negotiateConnectionWithClientOffer = negotiateConnectionWithClientOffer;
exports.negotiateIngestConnectionWithClientOffer = negotiateIngestConnectionWithClientOffer;
exports.useWebRTCIngest = useWebRTCIngest;
exports.forceStereoAudio = forceStereoAudio;
const tslib_1 = require("tslib");
const react_1 = require("react");
const sdpTransform = tslib_1.__importStar(require("sdp-transform"));
const streamplace_1 = require("streamplace");
const __1 = require("../..");
const webrtc_primitives_1 = require("./webrtc-primitives");
function useWebRTC(streamer) {
    const [mediaStream, setMediaStream] = (0, react_1.useState)(null);
    const [stuck, setStuck] = (0, react_1.useState)(false);
    const setStatus = (0, __1.usePlayerStore)((x) => x.setStatus);
    let agent = (0, __1.usePossiblyUnauthedPDSAgent)();
    const myDID = (0, __1.useDID)();
    const myHandle = (0, __1.useHandle)();
    const playbackWorkerUrl = (0, __1.useStreamplaceStore)((state) => state.playbackWorkerUrl);
    const isOwnStream = !!(myDID &&
        (streamer === myDID || streamer === myHandle));
    const lastChange = (0, react_1.useRef)(0);
    (0, react_1.useEffect)(() => {
        if (!agent) {
            return;
        }
        const peerConnection = new webrtc_primitives_1.RTCPeerConnection({
            bundlePolicy: "max-bundle",
        });
        peerConnection.addTransceiver("video", {
            direction: "recvonly",
        });
        peerConnection.addTransceiver("audio", {
            direction: "recvonly",
        });
        peerConnection.addEventListener("track", (event) => {
            const track = event.track;
            if (!track) {
                return;
            }
            setMediaStream(event.streams[0]);
        });
        peerConnection.addEventListener("connectionstatechange", () => {
            console.log("connection state change", peerConnection.connectionState);
            if (peerConnection.connectionState === "closed" ||
                peerConnection.connectionState === "failed" ||
                peerConnection.connectionState === "disconnected") {
                console.log("setting stuck to true", peerConnection.connectionState);
                setStuck(true);
            }
            if (peerConnection.connectionState !== "connected") {
                return;
            }
        });
        peerConnection.addEventListener("negotiationneeded", () => {
            negotiateConnectionWithClientOffer(peerConnection, streamer, undefined, agent, isOwnStream, playbackWorkerUrl);
        });
        let lastFramesReceived = 0;
        let lastAudioFramesReceived = 0;
        const handle = setInterval(async () => {
            const stats = await peerConnection.getStats();
            stats.forEach((stat) => {
                const mediaType = stat.mediaType /* web */ ?? stat.kind; /* native */
                if (stat.type === "inbound-rtp" && mediaType === "audio") {
                    const audioFramesReceived = stat.lastPacketReceivedTimestamp;
                    if (lastAudioFramesReceived !== audioFramesReceived) {
                        lastAudioFramesReceived = audioFramesReceived;
                        lastChange.current = Date.now();
                        setStatus(__1.PlayerStatus.PLAYING);
                        setStuck(false);
                    }
                }
                if (stat.type === "inbound-rtp" && mediaType === "video") {
                    const framesReceived = stat.framesReceived;
                    if (lastFramesReceived !== framesReceived) {
                        lastFramesReceived = framesReceived;
                        lastChange.current = Date.now();
                        setStatus(__1.PlayerStatus.PLAYING);
                        setStuck(false);
                    }
                }
            });
            if (Date.now() - lastChange.current > 2000) {
                setStuck(true);
            }
        }, 200);
        return () => {
            clearInterval(handle);
            peerConnection.close();
        };
    }, [streamer, agent, isOwnStream, playbackWorkerUrl]);
    return [mediaStream, stuck];
}
/**
 * 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
 */
async function negotiateConnectionWithClientOffer(peerConnection, streamer, bearerToken, agent, isOwnStream, playbackWorkerUrl) {
    /** https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/createOffer */
    const offer = await peerConnection.createOffer({
        offerToReceiveAudio: true,
        offerToReceiveVideo: true,
    });
    if (!offer.sdp) {
        throw Error("no SDP in offer");
    }
    const newSDP = forceStereoAudio(offer.sdp);
    offer.sdp = newSDP;
    /** https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/setLocalDescription */
    await peerConnection.setLocalDescription(offer);
    /** Wait for ICE gathering to complete */
    let 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") {
        try {
            /**
             * 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.
             */
            let response = await postSDPOffer(streamer, ofr.sdp, bearerToken, agent, isOwnStream, playbackWorkerUrl);
            let text = new TextDecoder().decode(response.data);
            if (response.success) {
                if (peerConnection.connectionState === "closed") {
                    return;
                }
                await peerConnection.setRemoteDescription(new webrtc_primitives_1.RTCSessionDescription({ type: "answer", sdp: text }));
                return "https://stream.place/example";
            }
            else {
                console.error(text);
            }
        }
        catch (e) {
            console.error(`posting sdp offer failed: ${e}`);
        }
        /** Limit reconnection attempts to at-most once every 5 seconds */
        await new Promise((r) => setTimeout(r, 5000));
    }
}
async function negotiateIngestConnectionWithClientOffer(peerConnection, endpoint, bearerToken) {
    /** https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/createOffer */
    const offer = await peerConnection.createOffer({
        offerToReceiveAudio: true,
        offerToReceiveVideo: true,
    });
    if (!offer.sdp) {
        throw Error("no SDP in offer");
    }
    const newSDP = forceStereoAudio(offer.sdp);
    offer.sdp = newSDP;
    /** https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/setLocalDescription */
    await peerConnection.setLocalDescription(offer);
    /** Wait for ICE gathering to complete */
    let 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") {
        try {
            /**
             * 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.
             */
            let response = await postSDPIngestOffer(endpoint, ofr.sdp, bearerToken);
            if (response.status === 201) {
                if (peerConnection.connectionState === "closed") {
                    return;
                }
                await peerConnection.setRemoteDescription(new webrtc_primitives_1.RTCSessionDescription({
                    type: "answer",
                    sdp: await response.text(),
                }));
                return "https://stream.place/example";
            }
            else {
                console.error(await response.text());
            }
        }
        catch (e) {
            console.error(`posting sdp offer failed: ${e}`);
        }
        /** Limit reconnection attempts to at-most once every 5 seconds */
        await new Promise((r) => setTimeout(r, 5000));
    }
}
async function getPlaybackServerAgent(agent, streamer, playbackWorkerUrl) {
    if (!playbackWorkerUrl) {
        return agent;
    }
    try {
        const lookupAgent = new streamplace_1.StreamplaceAgent(playbackWorkerUrl);
        const res = await lookupAgent.place.stream.playback.getPlaybackServer({
            stream: streamer,
        });
        if (res.data.servers.length > 0) {
            const serverUrl = res.data.servers[0];
            console.log(`Using playback server: ${serverUrl}`);
            return new streamplace_1.StreamplaceAgent(serverUrl);
        }
    }
    catch (e) {
        console.error("getPlaybackServer failed, using default agent:", e);
    }
    return agent;
}
async function postSDPOffer(streamer, data, bearerToken, agent, isOwnStream, playbackWorkerUrl) {
    if (!agent) {
        throw new Error("No agent found");
    }
    // Own stream: use the authenticated PDS agent directly (needed for
    // unpublished stream preview). Otherwise, look up a playback server
    // and use an anonymous agent for it.
    const playbackAgent = isOwnStream
        ? agent
        : await getPlaybackServerAgent(agent, streamer, playbackWorkerUrl);
    return await playbackAgent.place.stream.playback.whep(data, {
        qp: {
            rendition: "source",
            streamer: streamer,
        },
    });
}
async function postSDPIngestOffer(endpoint, data, bearerToken) {
    return await fetch(endpoint, {
        method: "POST",
        mode: "cors",
        headers: {
            "content-type": "application/sdp",
            ...(bearerToken ? { Authorization: `Bearer ${bearerToken}` } : {}),
        },
        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) {
    return new Promise((resolve) => {
        /** Wait at most 1 second for ICE gathering. */
        setTimeout(function () {
            if (peerConnection.connectionState === "closed") {
                return;
            }
            resolve(peerConnection.localDescription);
        }, 1000);
        peerConnection.addEventListener("icegatheringstatechange", (ev) => {
            if (peerConnection.iceGatheringState === "complete") {
                resolve(peerConnection.localDescription);
            }
        });
    });
}
function useWebRTCIngest({ endpoint, }) {
    const [mediaStream, setMediaStream] = (0, react_1.useState)(null);
    const ingestConnectionState = (0, __1.usePlayerStore)((x) => x.ingestConnectionState);
    const setIngestConnectionState = (0, __1.usePlayerStore)((x) => x.setIngestConnectionState);
    const storedKey = (0, __1.useStreamKey)();
    (0, react_1.useEffect)(() => {
        if (storedKey?.error) {
            console.error("error creating stream key", storedKey.error);
        }
    }, [storedKey?.error]);
    const [peerConnection, setPeerConnection] = (0, react_1.useState)(null);
    const videoTransceiver = (0, react_1.useRef)(null);
    const audioTransceiver = (0, react_1.useRef)(null);
    const [retryTime, setRetryTime] = (0, react_1.useState)(0);
    const ingestLive = (0, __1.usePlayerStore)((x) => x.ingestLive);
    // "Outer loop": when we need a new peer connection, this sets that up
    (0, react_1.useEffect)(() => {
        if (!storedKey) {
            return;
        }
        if (!ingestLive) {
            return;
        }
        const peerConnection = new webrtc_primitives_1.RTCPeerConnection({
            bundlePolicy: "max-bundle",
        });
        videoTransceiver.current = peerConnection.addTransceiver("video", {
            direction: "sendonly",
        });
        audioTransceiver.current = peerConnection.addTransceiver("audio", {
            direction: "sendonly",
        });
        peerConnection.addEventListener("connectionstatechange", (ev) => {
            setIngestConnectionState(peerConnection.connectionState);
            console.log("connection state change", peerConnection.connectionState);
            if (peerConnection.connectionState === "failed") {
                setRetryTime(Date.now());
            }
        });
        peerConnection.addEventListener("negotiationneeded", (ev) => {
            if (!storedKey?.streamKey?.privateKey) {
                throw new Error("no private key found");
            }
            negotiateIngestConnectionWithClientOffer(peerConnection, endpoint, storedKey.streamKey?.privateKey);
        });
        peerConnection.addEventListener("track", (ev) => {
            console.log(`got peerconnection track with ${ev.track.kind}`, ev.track.id);
            // console.log(ev);
        });
        setPeerConnection(peerConnection);
        return () => {
            peerConnection.close();
        };
    }, [endpoint, storedKey.streamKey?.privateKey, retryTime, ingestLive]);
    // "Inner loop": when our tracks change, we update the transceivers
    (0, react_1.useEffect)(() => {
        if (!mediaStream) {
            return;
        }
        if (!peerConnection) {
            return;
        }
        if (!ingestLive) {
            return;
        }
        for (const track of mediaStream.getTracks()) {
            console.log("adding track", track.kind, track.label, track.enabled, track.readyState);
            if (track.kind === "video") {
                videoTransceiver.current?.sender?.replaceTrack(track);
            }
            else if (track.kind === "audio") {
                audioTransceiver.current?.sender?.replaceTrack(track);
            }
        }
    }, [peerConnection, mediaStream, ingestLive]);
    return [mediaStream, setMediaStream];
}
function forceStereoAudio(sdp) {
    const parsedSDP = sdpTransform.parse(sdp);
    const audioMedia = parsedSDP.media.find((m) => m.type === "audio");
    if (!audioMedia) {
        throw Error("no audio media in SDP");
    }
    const opusCodec = audioMedia.rtp.find((c) => c.codec === "opus");
    if (!opusCodec) {
        throw Error("no opus codec in SDP");
    }
    const opusFMTP = audioMedia.fmtp.find((c) => c.payload === opusCodec.payload);
    if (!opusFMTP) {
        throw Error("no opus fmtp in SDP");
    }
    const opusParams = sdpTransform.parseParams(opusFMTP.config);
    opusParams.stereo = 1;
    const newParams = Object.entries(opusParams)
        .map(([k, v]) => `${k}=${v}`)
        .join(";");
    opusFMTP.config = newParams;
    return sdpTransform.write(parsedSDP);
}
//# sourceMappingURL=use-webrtc.js.map