VaKeR CYBER ARMY
Logo of a company Server : Apache/2.4.41 (Ubuntu)
System : Linux absol.cf 5.4.0-198-generic #218-Ubuntu SMP Fri Sep 27 20:18:53 UTC 2024 x86_64
User : www-data ( 33)
PHP Version : 7.4.33
Disable Function : pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,pcntl_unshare,
Directory :  /var/www/html/webrtc/js/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Current File : //var/www/html/webrtc/js/webrtc-v2.js
(()=>{

'use strict';

// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// WebRTC Simple Calling API + Mobile
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
const PHONE = window.PHONE = config => {
    const PHONE         = ()=>{};
    const pubnub        = socket(config);
    const pubkey        = config.publish_key   || 'demo';
    const subkey        = config.subscribe_key || 'demo';
    const autocam       = config.autocam !== false;
    const sessionid     = uuid();
    const mediaconf     = config.media || { audio : true, video : true };
    const conversations = {};
    let   myvideo       = document.createElement('video');
    let   snapper       = ()=>' ';
    let   mystream      = null;
    let   myconnection  = false;

    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    // RTC Peer Connection Session (one per call)
    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    const PeerConnection =
        window.RTCPeerConnection    ||
        window.mozRTCPeerConnection ||
        window.webkitRTCPeerConnection;

    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    // ICE (many route options per call)
    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    const IceCandidate =
        window.RTCIceCandidate ||
        window.mozRTCIceCandidate;

    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    // Media Session Description (offer and answer per call)
    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    const SessionDescription =
        window.RTCSessionDescription    ||
        window.mozRTCSessionDescription ||
        window.webkitRTCSessionDescription;

    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    // STUN Server List Configuration (public STUN list)
    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    const rtcconfig = { iceServers : [{ "urls" :
        navigator.mozGetUserMedia    ? "stun:stun.services.mozilla.com" :
        navigator.webkitGetUserMedia ? "stun:stun.l.google.com:19302"   :
                                       "stun:23.21.150.121"
    }
    //,   {urls: "stun:stun.l.google.com:19302"}
    ,   {urls: "stun:stun1.l.google.com:19302"}
    ,   {urls: "stun:stun2.l.google.com:19302"}
    ,   {urls: "stun:stun3.l.google.com:19302"}
    //,   {urls: "stun:stun4.l.google.com:19302"}
    //,   {urls: "stun:23.21.150.121"}
    //,   {urls: "stun:stun01.sipphone.com"}
    //,   {urls: "stun:stun.ekiga.net"}
    //,   {urls: "stun:stun.fwdnet.net"}
    //,   {urls: "stun:stun.ideasip.com"}
    //,   {urls: "stun:stun.iptel.org"}
    //,   {urls: "stun:stun.rixtelecom.se"}
    //,   {urls: "stun:stun.schlund.de"}
    //,   {urls: "stun:stunserver.org"}
    //,   {urls: "stun:stun.softjoys.com"}
    //,   {urls: "stun:stun.voiparound.com"}
    //,   {urls: "stun:stun.voipbuster.com"}
    //,   {urls: "stun:stun.voipstunt.com"}
    //,   {urls: "stun:stun.voxgratia.org"}
    //,   {urls: "stun:stun.xten.com"}
    ] };

    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    // Custom STUN Options
    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    function add_servers(servers) {
        if (servers.constructor === Array)
            [].unshift.apply(rtcconfig.iceServers, servers);
        else rtcconfig.iceServers.unshift(servers);
    }

    if ('servers' in config) add_servers(config.servers);

    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    // PHONE Events
    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    let messagecb    = ()=>{};
    let readycb      = ()=>{};
    let cameracb     = ()=>{};
    let unablecb     = ()=>{};
    let debugcb      = ()=>{};
    let connectcb    = ()=>{};
    let disconnectcb = ()=>{};
    let reconnectcb  = ()=>{};
    let callstatuscb = ()=>{};
    let receivercb   = ()=>{};

    PHONE.camera     = cb => cameracb     = cb;
    PHONE.message    = cb => messagecb    = cb;
    PHONE.ready      = cb => readycb      = cb;
    PHONE.unable     = cb => unablecb     = cb;
    PHONE.callstatus = cb => callstatuscb = cb;
    PHONE.debug      = cb => debugcb      = cb;
    PHONE.connect    = cb => connectcb    = cb;
    PHONE.disconnect = cb => disconnectcb = cb;
    PHONE.reconnect  = cb => reconnectcb  = cb;
    PHONE.receive    = cb => receivercb   = cb;

    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    // Add/Get Conversation - Creates a new PC or Returns Existing PC
    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    function get_conversation(number) {
        let talk = conversations[number] || (number => {
            let talk = {
                number  : number
            ,   status  : ''
            ,   image   : document.createElement('img')
            ,   started : +new Date
            ,   imgset  : false
            ,   imgsent : 0
            ,   pc      : new PeerConnection(rtcconfig)
            ,   closed  : false
            ,   usermsg : ()=>{}
            ,   thumb   : null
            ,   connect : ()=>{}
            ,   end     : ()=>{}
            };

            // Setup Event Methods
            talk.pc.ontrack        = config.ontrack || function(event) { onaddstream( number, event ) };;
            talk.pc.onicecandidate = function(event) { onicecandidate( number, event ) };
            talk.pc.number         = number;

            // Disconnect and Hangup
            talk.hangup = signal => {
                if (talk.closed) return;

                talk.closed = true;
                talk.imgset = false;

                if (signal !== false) transmit( number, { hangup : true } );

                talk.end(talk);
                talk.pc.close();
                close_conversation(number);
            };
           
            // Disconnected?
            talk.pc.oniceconnectionstatechange = function() {
                if(talk.pc.iceConnectionState == 'disconnected') {							
                    talk.hangup();									
                }
            }

            // Stop Audio/Video Stream
            talk.stop = () => {
                if (mystream) stopcamera();
                return mystream;
            };

            // Sending Messages
            talk.send = message => {
                transmit( number, { usermsg : message } );
            };

            // Sending Stanpshots
            talk.snap = () => {
                let pic = snapper();
                transmit( number, { thumbnail : pic } );
                let img = document.createElement('img');
                img.src = pic;
                return { data : pic, image : img };
            };

            // Take One Snapshot
            talk.snap();

            // Nice Accessor to Update Disconnect & Establis CBs
            talk.thumbnail = cb => {talk.thumb   = cb; return talk};
            talk.ended     = cb => {talk.end     = cb; return talk};
            talk.connected = cb => {talk.connect = cb; return talk};
            talk.message   = cb => {talk.usermsg = cb; return talk};

            // Add Local Media Streams Audio Video Mic Camera
            if (mystream) mystream.getTracks().forEach(
                track => talk.pc.addTrack( track, mystream )
            );

            // Notify of Call Status
            update_conversation( talk, 'connecting' );

            // Return Brand New Talk Reference
            conversations[number] = talk;
            return talk;
        })(number);

        // Return Existing or New Reference to Caller
        return talk;
    }

    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    // Remove Conversation
    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    function close_conversation(number) {
        conversations[number] = null;
        delete conversations[number];
    }

    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    // Notify of Call Status Events
    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    function update_conversation( talk, status ) {
        talk.status = status;
        callstatuscb(talk);
        return talk;
    }

    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    // UUID
    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    PHONE.uuid = uuid;

    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    // DOM Helper Functions
    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    PHONE.$  = el => document.getElementById(el);
    PHONE.$$ = el => document.getElementsByTagName(el);

    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    // DOM Bind
    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    PHONE.bind = ( type, el, fun ) => {
        type.split(',').forEach( etype => {
            const rapfun = e => {
                if (!e) e = window.event;
                if (!fun(e)) {
                    e.cancelBubble = true;
                    e.preventDefault  && e.preventDefault();
                    e.stopPropagation && e.stopPropagation();
                }
            };

            if ( el.addEventListener ) el.addEventListener(
                etype, rapfun, false
            );
            else if ( el.attachEvent ) el.attachEvent( 'on' + etype, rapfun );
            else  el[ 'on' + etype ] = rapfun;
        } );
    };

    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    // DOM UNBind
    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    PHONE.unbind = ( type, el, fun ) => {
        if ( el.removeEventListener ) el.removeEventListener( type, false );
        else if ( el.detachEvent ) el.detachEvent( 'on' + type, false );
        else  el[ 'on' + type ] = null;
    }

    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    // Get Number
    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    PHONE.number = () => {
        return config.number;
    };

    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    // Get Call History
    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    PHONE.history = settings => {
        pubnub.history({
            channel  : settings[number],
            callback : call_history => {
                settings['history'](call_history[0]);
            }
        })
    };

    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    // Make Call - Create new PeerConnection
    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    PHONE.dial = function( number, servers ) {
        if (!!servers) add_servers(servers);
        if (!number) return debugcb("Missing Number to Dial.");

        let talk = get_conversation(number);
        let pc   = talk.pc;

        // Prevent Repeat Calls
        if (talk.dialed) return false;
        talk.dialed = true;

        // Send SDP Offer (Call)
        pc.createOffer().then( async offer => {
            debugcb(["pc.createOffer(PASS)", await pc.setLocalDescription(offer)]);
            transmit( number, offer );
        } ).catch( error => debugcb(["pc.createOffer(FAIL)", error]) );

        // Return Session Reference
        return talk;
    };

    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    // Send Image Snap - Send Image Snap to All Calls or a Specific Call
    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    PHONE.snap = function( message, number ) {
        if (number) return get_conversation(number).snap(message);
        let pic = {};
        for (let number in conversations) pic = conversations[number].snap();
        return pic;
    };

    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    // Send Message - Send Message to All Calls or a Specific Call
    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    PHONE.send = ( message, number ) => {
        if (number) return get_conversation(number).send(message);
        for (let number in conversations) conversations[number].send(message);
    };

    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    // End Call - Close All Calls or a Specific Call
    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    PHONE.hangup = number => {
        if (number) return get_conversation(number).hangup();
        for (let number in conversations) conversations[number].hangup();
    };

    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    // Auto-hangup on Leave
    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    PHONE.bind( 'unload,beforeunload', window, () => {
        if (PHONE.goodbye) return true;
        PHONE.goodbye = true;

        for (let number in conversations) {
            let talk     = conversations[number];
            let mynumber = config.number;
            let packet   = { hangup:true };
            let message  = { packet:packet, id:sessionid, number:mynumber };
            let client   = new XMLHttpRequest();
            let url      = 'https://pubsub.pubnub.com/publish/'
                           + pubkey + '/'
                           + subkey + '/0/'
                           + number + '/0/'
                           + JSON.stringify(message);

            client.open( 'GET', url, false );
            client.send();
            talk.hangup();
        }

        return true;
    } );

    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    // Grab Local Video Snapshot
    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    function snapshots_setup(stream) {
        let video   = myvideo || document.createElement('video');
        let canvas  = document.createElement('canvas');
        let context = canvas.getContext("2d");
        let snap    = { width: 240, height: 180 };

        // Video Settings
        video.width     = snap.width;
        video.height    = snap.height;
        video.srcObject = stream;
        video.volume    = 0.0;

        // Start Video Stream
        try { video.play() }
        catch(e) { debugcb(["video.play(FAIL)", e]) }

        // Canvas Settings
        canvas.width  = snap.width;
        canvas.height = snap.height;

        // Capture Local Pic
        snapper = () => {
            try {
                context.drawImage( video, 0, 0, snap.width, snap.height );
            } catch(e) { debugcb(["context.drawImage(FAIL)", e]) }
            return canvas.toDataURL( 'image/jpeg', 0.30 );
        };
    }

    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    // Visually Display New Stream
    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    function onaddstream( number, obj ) {
        let vid    = document.createElement('video');
        let talk   = get_conversation(number);

        vid.setAttribute( 'autoplay',    'autoplay'    );
        vid.setAttribute( 'playsinline', 'playsinline' );
        vid.srcObject = obj.streams[0];

        talk.video = vid;
        talk.connect(talk);
    }

    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    // On ICE Route Candidate Discovery
    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    function onicecandidate( number, event ) {
        if (!event.candidate) return;
        transmit( number, event.candidate );
    };

    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    // Listen For New Incoming Calls
    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    function dailer_subscribe() {
        pubnub.subscribe({
            restore    : false
        ,   channel    : config.number
        ,   message    : receive
        ,   disconnect : disconnectcb
        ,   reconnect  : reconnectcb
        ,   connect    : () => onready(true)
        });
    }

    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    // When Ready to Receive Calls
    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    function onready(subscribed) {
        if (subscribed) myconnection = true;
        if (myconnection && autocam) readycb();
        if (!(mystream && myconnection)) return;

        connectcb();
        if (!autocam) readycb();
    }

    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    // Prepare Local Media Camera and Mic
    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    function startcamera() {
        navigator.mediaDevices.getUserMedia(mediaconf).then( stream => {
            if (!stream) return unablecb(stream);
            mystream = stream;
            snapshots_setup(stream);
            onready();
            cameracb(myvideo);
        } ).catch( info => {
            debugcb(["navigator.mediaDevices.getUserMedia(FAIL)", info]);
            return unablecb(info);
        } );
    }

    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    // Stop Camera/Mic
    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    function stopcamera() {
        if (!mystream) return;
        for (let track of mystream.getTracks()) track.stop();
    }

    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    // Toggle Mic
    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    function toggleAudio() {
        if (!mystream) return;
        for (let track of mystream.getAudioTracks() ){
            track.enabled = !track.enabled ;
        }
    }

    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    // Toggle Camera
    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    function toggleVideo() {
        if (!mystream) return;
        for (let track of mystream.getVideoTracks() ){
            track.enabled = !track.enabled ;
        }
    }
    
    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    // Initiate Dialing Socket
    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    function startsubscribe() {
        onready();
        dailer_subscribe();
    }

    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    // Send SDP Call Offers/Answers and ICE Candidates to Peer
    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    function transmit( phone, packet ) {
        if (!packet) return;
        let number  = config.number;
        let message = { packet : packet, id : sessionid, number : number };
        debugcb(["transmit(PASS)", message]);
        pubnub.publish({ channel : phone, message : message });
    }

    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    // SDP Offers & ICE Candidates Receivable Processing
    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    function receive(message) {
        // Debug Callback of Data to Watch
        debugcb(["receive(PASS)", message]);

        // Get Call Reference
        let talk = get_conversation(message.number);

        // Ignore if Closed
        if (talk.closed) return;

        // User Message
        if (message.packet.usermsg) {
            messagecb( talk, message.packet.usermsg );
            return talk.usermsg( talk, message.packet.usermsg );
        }

        // Thumbnail Preview Image
        if (message.packet.thumbnail) return create_thumbnail(message);

        // If Hangup Request
        if (message.packet.hangup) return talk.hangup(false);

        // If Peer Calling Inbound (Incoming)
        if ( message.packet.sdp && !talk.received ) {
            talk.received = true;
            receivercb(talk);
        }

        // Update Peer Connection with SDP Offer or ICE Routes
        if (message.packet.sdp) add_sdp_offer(message);
        else                    add_ice_route(message);
    }

    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    // Create Remote Friend Thumbnail
    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    function create_thumbnail(message) {
        let talk       = get_conversation(message.number);
        talk.image.src = message.packet.thumbnail;

        // Call only once
        if (!talk.thumb) return;
        if (!talk.imgset) talk.thumb(talk);
        talk.imgset = true;
    }

    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    // Add SDP Offer/Answers
    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    async function add_sdp_offer(message) {
        // Get Call Reference
        let talk = get_conversation(message.number);
        let pc   = talk.pc;
        let type = message.packet.type == 'offer' ? 'offer' : 'answer';

        // Deduplicate SDP Offerings/Answers
        if (type in talk) return;
        talk[type]  = true;
        talk.dialed = true;

        // Notify of Call Status
        update_conversation( talk, 'routing' );

        // Add SDP Offer/Answer
        pc.setRemoteDescription(new SessionDescription(message.packet))
        .then( () => {
            // Set Connected Status
            update_conversation( talk, 'connected' );

            // Call Online and Ready
            if (pc.remoteDescription.type != 'offer') return;

            // Create Answer to Call
            pc.createAnswer().then( async answer => {
                debugcb([
                    "pc.createAnswer(PASS)",
                    await pc.setLocalDescription(answer)
                ]);
                transmit( message.number, answer );
            } ).catch( error => debugcb(["pc.createAnswer(FAIL)", error]) );
        } ).catch( error => debugcb(["pc.setRemoteDescription(FAIL)", error]) );
    }

    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    // Add ICE Candidate Routes
    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    function add_ice_route(message) {
        // Leave if Non-good ICE Packet
        if (!message.packet)           return;
        if (!message.packet.candidate) return;

        // Get Call Reference
        let talk = get_conversation(message.number);
        let pc   = talk.pc;

        // Add ICE Candidate Routes
        pc.addIceCandidate(new IceCandidate(message.packet))
        .then( info => debugcb(["pc.addIceCandidate(PASS)", info, message]))
        .catch( err => debugcb(["pc.addIceCandidate(FAIL)", err, message]) );
    }

    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    // Main - Setup Dialer Socket and Camera
    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    PHONE.startcamera        = startcamera;
    PHONE.camera.start       = startcamera;
    PHONE.camera.stop        = stopcamera;
    PHONE.camera.toggleAudio = toggleAudio;
    PHONE.camera.toggleVideo = toggleVideo;
    PHONE.camera.video       = () => myvideo;
    PHONE.camera.ready       = PHONE.camera;

    // Start Camera Automatically
    if (autocam) startcamera();

    // Start Dailer Socket
    startsubscribe();

    // Return Phone API
    return PHONE;
};

// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// UUID
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
function uuid(callback) {
    let u = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g,
    function(c) {
        let r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
        return v.toString(16);
    });
    if (callback) callback(u);
    return u;
}



// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
// PubNub Socket Lib
// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
function socket(setup) {
    const pubkey = setup.publish_key   || setup.pubkey || 'demo'
    ,     subkey = setup.subscribe_key || setup.subkey || 'demo';

    // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    // Publish
    // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    let publishing     = false;
    const publishQueue = [];

    function publish(data) {
        publishQueue.push(data);
        publishSender();
    }

    // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    // Publish Queue Worker
    // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    function publishSender() {
        if (publishing) return;

        let data = publishQueue.shift();
        if (!data) return (publishing = false);
        publishing = true;

        const publisher = requester({
            timeout : 5000
        ,   success : () => { publishing = false; publishSender() }
        ,   fail    : () => publishQueue.unshift(data)
        });

        let url = ['https://pubsub.pubnub.com/publish'
                  , pubkey
                  , subkey,       '0'
                  , data.channel, '0'
                  , encodeURIComponent(JSON.stringify(data.message))
                  ].join('/');

        publisher({ url : url });
    }


    // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    // Subscribe
    // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    function subscribe(setup) {
        let channel    = setup.channel       || 'a'
        ,   message    = setup.message       || (()=>{})
        ,   connect    = setup.connect       || (()=>{})
        ,   disconnect = setup.disconnect    || (()=>{})
        ,   reconnect  = setup.reconnect     || (()=>{})
        ,   timetoken  = setup.timetoken     || '0'
        ,   timeout    = setup.timeout       || 300000
        ,   windy      = setup.windowing     || 10
        ,   restore    = setup.restore 
        ,   windowing  = 10
        ,   connected  = true
        ,   stop       = false
        ,   url        = ''
        ,   origin     = 'ps'+(Math.random()+'').split('.')[1]+'.pubnub.com';

        // Requester Object
        let request = requester({
            timeout : timeout,
            success : next,
            fail    : () => next()
        });

        // Subscribe Loop
        function next(payload) { 
            if (stop) return;
            if (payload) {
                if (+timetoken < 100000) connect();
                if (!connected)          reconnect();

                connected = true;

                if (!restore) timetoken = payload.t.t;
                else {
                    timetoken = '1000';
                    restore   = false;
                }

                payload.m.forEach( msg => message( msg.d, msg ) );
            }
            else {
                if (connected) disconnect();
                connected = false;
            }

            url = [
                'https://',       origin, 
                '/v2/subscribe/', subkey,
                '/',              channel,
                '/0/',            timetoken
            ].join('');

            setTimeout( () => {
                windowing = windy;
                request({ url : url });
            }, windowing );
        }

        // Cancel Subscription
        function unsubscribe() { stop = true }

        // Start Subscribe Loop
        next();

        // Allow Cancelling Subscriptions
        return { unsubscribe : unsubscribe };
    }

    // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    // History
    // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    function history(data) {
        // TODO
    }

    // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    // Request URL
    // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    function requester(setup) {
        let xhr      = new XMLHttpRequest()
        ,   finished = false
        ,   timeout  = setup.timeout || 5000
        ,   success  = setup.success || function(){}
        ,   fail     = setup.fail    || function(){};

        // Cancel a Pending Request
        function abort() {
            if (finished) return;
            xhr.abort && xhr.abort();
            finish();
        }

        // Mark Request as Completed
        function finish() {
            finished = true;
        }

        // When a Request has a Payload
        xhr.onload = () => {
            if (finished) return;
            finish();
            let result;

            try      { result = JSON.parse(xhr.response) }
            catch(e) { fail(xhr) }

            if (result) success(result);
            else        fail(xhr);
            result = null;
        };

        // When a Request has Failed
        xhr.onabort = xhr.ontimeout = xhr.onerror = () => {
            if (finished) return;
            finish();
            fail(xhr);
        };

        // Timeout and Aboart for Slow Requests
        xhr.timer = setTimeout( () => {
            if (finished) return;
            abort();
            fail(xhr);
        }, timeout );

        // Return Requester Object
        return setup => {
            let url     = setup.url     || 'https://ps.pubnub.com/time/0'
            ,   headers = setup.headers || {}
            ,   method  = setup.method  || 'GET'
            ,   payload = setup.payload || null
            ,   params  = setup.params  || setup.data || {}
            ,   data    = [];

            // URL Parameters
            for (let param in params)
                data.push([ param, params[param] ].join('='));

            // Start Request
            finished = false;
            xhr.timeout = timeout;
            xhr.open(
                method,
                url + (data.length ? ('?' + data.join('&')) : ''),
                true
            );

            // Headers
            for (let header in headers)
                xhr.setRequestHeader( header, headers[header] );

            // Send Request
            xhr.send(payload);

            return {
                xhr   : xhr,
                abort : abort
            } 
        };
    }

    // Return Socket Lib Instance
    return {
        publish   : publish
    ,   subscribe : subscribe
    ,   history   : history
    };

}

})();

VaKeR 2022