var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { isDevEnvironment, showBalloonError, showBalloonWarning } from "../engine/debug/index.js";
import { Application } from "../engine/engine_application.js";
import { RoomEvents } from "../engine/engine_networking.js";
import { disposeStream, NetworkedStreamEvents, NetworkedStreams } from "../engine/engine_networking_streams.js";
import { serializable } from "../engine/engine_serialization_decorator.js";
import { DeviceUtilities, getParam } from "../engine/engine_utils.js";
import { delay } from "../engine/engine_utils.js";
import { getIconElement } from "../engine/webcomponents/icons.js";
import { Behaviour } from "./Component.js";
export const noVoip = "noVoip";
const debugParam = getParam("debugvoip");
/**
 * [Voip](https://engine.needle.tools/docs/api/Voip) Voice over IP (VoIP) component for real-time audio communication between users.
 * Allows sending and receiving audio streams in networked rooms.
 *
 * **Requirements:**
 * - Active network connection (via {@link SyncedRoom} or manual connection)
 * - User permission for microphone access (requested automatically)
 * - HTTPS connection (required for WebRTC)
 *
 * **Features:**
 * - Automatic connection when joining rooms (`autoConnect`)
 * - Background audio support (`runInBackground`)
 * - Optional UI toggle button (`createMenuButton`)
 * - Mute/unmute control
 *
 * **Debug:** Use `?debugvoip` URL parameter or set `debug = true` for logging.
 * Press 'v' to toggle mute, 'c' to connect/disconnect when debugging.
 *
 * @example Enable VoIP in your scene
 * ```ts
 * const voip = myObject.addComponent(Voip);
 * voip.autoConnect = true;
 * voip.createMenuButton = true;
 *
 * // Manual control
 * voip.connect();    // Start sending audio
 * voip.disconnect(); // Stop sending
 * voip.setMuted(true); // Mute microphone
 * ```
 *
 * @summary Voice over IP for networked audio communication
 * @category Networking
 * @group Components
 * @see {@link SyncedRoom} for room management
 * @see {@link NetworkedStreams} for the underlying streaming
 * @see {@link ScreenCapture} for video streaming
 * @link https://engine.needle.tools/docs/networking.html
 */
export class Voip extends Behaviour {
    /** When enabled, VoIP will start when a room is joined or when this component is enabled while already in a room.
     * @default true
    */
    autoConnect = true;
    /**
     * When enabled, VoIP will stay connected even when the browser tab is not focused/active anymore.
     * @default true
     */
    runInBackground = true;
    /**
     * When enabled, a menu button will be created to allow the user to toggle VoIP on and off.
     */
    createMenuButton = true;
    /**
     * When enabled debug messages will be printed to the console. This is useful for debugging audio issues. You can also append ?debugvoip to the URL to enable this.
     */
    debug = false;
    _net;
    _menubutton;
    /** @internal */
    awake() {
        if (debugParam)
            this.debug = true;
        if (this.debug) {
            console.log("VOIP debugging: press 'v' to toggle mute or 'c' to toggle connect/disconnect");
            window.addEventListener("keydown", async (evt) => {
                const key = evt.key.toLowerCase();
                switch (key) {
                    case "v":
                        console.log("MUTE?", !this.isMuted);
                        this.setMuted(!this.isMuted);
                        break;
                    case "c":
                        if (this.isSending)
                            this.disconnect();
                        else
                            this.connect();
                        break;
                }
            });
            // mute unfocused
            window.addEventListener("blur", () => {
                console.log("VOIP: MUTE ON BLUR");
                this.setMuted(true);
            });
            window.addEventListener("focus", () => {
                console.log("VOIP: UNMUTE ON FOCUS");
                this.setMuted(false);
            });
        }
    }
    /** @internal */
    onEnable() {
        if (!this._net)
            this._net = NetworkedStreams.create(this);
        if (this.debug)
            this._net.debug = true;
        this._net.addEventListener(NetworkedStreamEvents.StreamReceived, this.onReceiveStream);
        this._net.addEventListener(NetworkedStreamEvents.StreamEnded, this.onStreamEnded);
        this._net.enable();
        if (this.autoConnect) {
            if (this.context.connection.isConnected)
                this.connect();
        }
        this.context.connection.beginListen(RoomEvents.JoinedRoom, this.onJoinedRoom);
        this.context.connection.beginListen(RoomEvents.LeftRoom, this.onLeftRoom);
        this.onEnabledChanged();
        this.updateButton();
        window.addEventListener("visibilitychange", this.onVisibilityChanged);
    }
    /** @internal */
    onDisable() {
        if (this._net) {
            this._net.stopSendingStream(this._outputStream);
            //@ts-ignore
            this._net.removeEventListener(NetworkedStreamEvents.StreamReceived, this.onReceiveStream);
            //@ts-ignore
            this._net.removeEventListener(NetworkedStreamEvents.StreamEnded, this.onStreamEnded);
            this._net?.disable();
        }
        this.context.connection.stopListen(RoomEvents.JoinedRoom, this.onJoinedRoom);
        this.context.connection.stopListen(RoomEvents.LeftRoom, this.onLeftRoom);
        this.onEnabledChanged();
        this.updateButton();
        window.removeEventListener("visibilitychange", this.onVisibilityChanged);
    }
    /** @internal */
    onDestroy() {
        this._menubutton?.remove();
        this._menubutton = undefined;
    }
    /** Set via the mic button (e.g. when the websocket connection closes and rejoins but the user was muted before we don't want to enable VOIP again automatically) */
    _allowSending = true;
    _outputStream = null;
    /**
     * @returns true if the component is currently sending audio
     */
    get isSending() { return this._outputStream != null && this._outputStream.active; }
    /** Start sending audio. */
    async connect(audioSource) {
        if (!this._net) {
            console.error("Cannot connect to voice chat - NetworkedStreams not initialized. Make sure the component is enabled before calling this method.");
            return false;
        }
        if (!this.context.connection.isConnected) {
            console.error("Cannot connect to voice chat - not connected to server");
            this.updateButton();
            return false;
        }
        else if (!await DeviceUtilities.microphonePermissionsGranted()) {
            console.error("Cannot connect to voice chat - microphone permissions not granted");
            this.updateButton();
            return false;
        }
        this._allowSending = true;
        this._net?.stopSendingStream(this._outputStream);
        disposeStream(this._outputStream);
        this._outputStream = await this.getAudioStream(audioSource);
        if (this._outputStream) {
            if (this.debug)
                console.log("VOIP: Got audio stream");
            this._net?.startSendingStream(this._outputStream);
            this.updateButton();
            return true;
        }
        else {
            this.updateButton();
            if (!await DeviceUtilities.microphonePermissionsGranted()) {
                showBalloonError("Microphone permissions not granted: Please grant microphone permissions to use voice chat");
            }
            else
                console.error("VOIP: Could not get audio stream - please make sure to connect an audio device and grant microphone permissions");
        }
        if (this.debug || isDevEnvironment())
            console.log("VOIP: Failed to get audio stream");
        return false;
    }
    /** Stop sending audio (muting your own microphone) */
    disconnect(opts) {
        if (opts?.remember) {
            this._allowSending = false;
        }
        this._net?.stopSendingStream(this._outputStream);
        disposeStream(this._outputStream);
        this._outputStream = null;
        this.updateButton();
    }
    /**
     * Mute or unmute the audio stream (this will only mute incoming streams and not mute your own microphone. Use disconnect() to mute your own microphone)
     */
    setMuted(mute) {
        const audio = this._outputStream?.getAudioTracks();
        if (audio) {
            for (const track of audio) {
                track.enabled = !mute;
            }
        }
    }
    /** Returns true if the audio stream is currently muted */
    get isMuted() {
        if (this._outputStream === null)
            return false;
        const audio = this._outputStream?.getAudioTracks();
        if (audio) {
            for (const track of audio) {
                if (!track.enabled)
                    return true;
            }
        }
        return false;
    }
    async updateButton() {
        if (this.createMenuButton) {
            if (!this._menubutton) {
                this._menubutton = document.createElement("button");
                this._menubutton.addEventListener("click", () => {
                    if (this.isSending) {
                        this.disconnect({ remember: true });
                    }
                    else
                        this.connect();
                    DeviceUtilities.microphonePermissionsGranted().then(res => {
                        if (!res)
                            showBalloonWarning("<strong>Microphone permissions not granted</strong>. Please allow your browser to use the microphone to be able to talk. Click on the button on the left side of your browser's address bar to allow microphone permissions.");
                    });
                });
            }
            if (this._menubutton) {
                this.context.menu.appendChild(this._menubutton);
                if (this.activeAndEnabled) {
                    this._menubutton.style.display = "";
                }
                else {
                    this._menubutton.style.display = "none";
                }
                this._menubutton.title = this.isSending ? "Click to disable your microphone" : "Click to enable your microphone";
                let label = this.isSending ? "" : "";
                let icon = this.isSending ? "mic" : "mic_off";
                const hasPermission = await DeviceUtilities.microphonePermissionsGranted();
                if (!hasPermission) {
                    label = "No Permission";
                    icon = "mic_off";
                    this._menubutton.title = "Microphone permissions not granted. Please allow your browser to use the microphone to be able to talk. This can usually be done in the addressbar of the webpage.";
                }
                this._menubutton.innerText = label;
                this._menubutton.prepend(getIconElement(icon));
                if (this.context.connection.isConnected == false)
                    this._menubutton.setAttribute("disabled", "");
                else
                    this._menubutton.removeAttribute("disabled");
            }
        }
        else if (!this.activeAndEnabled) {
            this._menubutton?.remove();
        }
    }
    // private _analyzer?: AudioAnalyser;
    /** @deprecated */
    getFrequency(_userId) {
        if (!this["unsupported_getfrequency"]) {
            this["unsupported_getfrequency"] = true;
            if (isDevEnvironment())
                showBalloonWarning("VOIP: getFrequency is currently not supported");
            console.warn("VOIP: getFrequency is currently not supported");
        }
        // null is get the first with some data
        // if (userId === null) {
        //     for (const c in this._incomingStreams) {
        //         const call = this._incomingStreams[c];
        //         if (call && call.currentAnalyzer) return call.currentAnalyzer.getAverageFrequency();
        //     }
        //     return null;
        // }
        // const call = this._incomingStreams.get(userId);
        // if (call && call.currentAnalyzer) return call.currentAnalyzer.getAverageFrequency();
        return null;
    }
    async getAudioStream(audio) {
        if (!navigator.mediaDevices.getUserMedia) {
            console.error("No getDisplayMedia support");
            return null;
        }
        const getUserMedia = async (constraints) => {
            return await navigator.mediaDevices.getUserMedia({ audio: constraints ?? true, video: false })
                .catch((err) => {
                console.warn("VOIP failed getting audio stream", err);
                return null;
            });
            ;
        };
        const stream = await getUserMedia(audio);
        if (!stream)
            return null;
        // NE-5445, on iOS after calling `getUserMedia` it automatically switches the audio to the built-in microphone and speakers even if headphones are connected
        // if there's no device selected explictly we will try to automatically select an external device
        if (DeviceUtilities.isiOS() && audio?.deviceId === undefined) {
            const devices = await navigator.mediaDevices.enumerateDevices();
            // select anything that doesn't have "iPhone" is likely "AirPods" or other bluetooth headphones
            const nonBuiltInAudioSource = devices.find((device) => (device.kind === "audioinput" || device.kind === "audiooutput") && !device.label.includes("iPhone"));
            if (nonBuiltInAudioSource) {
                const constraints = Object.assign({}, audio);
                constraints.deviceId = nonBuiltInAudioSource.deviceId;
                return await getUserMedia(constraints);
            }
        }
        return stream;
    }
    // we have to wait for the user to connect to a room when "auto connect" is enabled
    onJoinedRoom = async () => {
        if (this.debug)
            console.log("VOIP: Joined room");
        // Wait a moment for user list to be populated
        await delay(300);
        if (this.autoConnect && !this.isSending && this._allowSending) {
            this.connect();
        }
    };
    onLeftRoom = () => {
        if (this.debug)
            console.log("VOIP: Left room");
        this.disconnect();
        for (const incoming of this._incomingStreams.values()) {
            disposeStream(incoming.srcObject);
        }
        this._incomingStreams.clear();
    };
    _incomingStreams = new Map();
    onReceiveStream = (evt) => {
        const userId = evt.target.userId;
        const stream = evt.stream;
        let audioElement = this._incomingStreams.get(userId);
        if (!audioElement) {
            audioElement = new Audio();
            this._incomingStreams.set(userId, audioElement);
        }
        audioElement.srcObject = stream;
        audioElement.setAttribute("autoplay", "true");
        // for mobile we need to wait for user interaction to play audio. Auto play doesnt work on android when the page is refreshed
        Application.registerWaitForInteraction(() => {
            audioElement?.play().catch((err) => {
                console.error("VOIP: Failed to play audio", err);
            });
        });
    };
    onStreamEnded = (evt) => {
        const existing = this._incomingStreams.get(evt.userId);
        disposeStream(existing?.srcObject);
        this._incomingStreams.delete(evt.userId);
    };
    onEnabledChanged = () => {
        for (const key of this._incomingStreams) {
            const element = key[1];
            element.muted = !this.enabled;
        }
    };
    onVisibilityChanged = () => {
        if (this.runInBackground)
            return;
        const visible = document.visibilityState === "visible";
        const muted = !visible;
        this.setMuted(muted);
        for (const element of this._incomingStreams) {
            const str = element[1];
            str.muted = muted;
        }
    };
}
__decorate([
    serializable()
], Voip.prototype, "autoConnect", void 0);
__decorate([
    serializable()
], Voip.prototype, "runInBackground", void 0);
__decorate([
    serializable()
], Voip.prototype, "createMenuButton", void 0);
//# sourceMappingURL=Voip.js.map