127.0.0.1:8000 watch-together / master server / utils / watchrooms.js
master

Tree @master (Download .tar.gz)

watchrooms.js @masterraw · history · blame

let watchrooms = {};

module.exports = {
    create: function(user_id, display_name, video_id) {
        watchrooms[video_id] = {
            // Lock to prevent multiple simultanious state updates
            update_state_timeout: undefined,
            play_invoked_at: undefined,
            state: {
                invoked_by: undefined,
                paused: true,
                currentTime: 0 // In seconds
            },
            host: user_id,
            viewers: {
                [user_id]: {
                    user_id: user_id,
                    display_name: display_name,
                    ready: false,
                    state: "Buffering...",
                    latency: 0
                }
            }
        };
        return watchrooms[video_id];
    },
    join: function(user_id, display_name, video_id) {
        watchrooms[video_id].viewers[user_id] = {
            user_id: user_id,
            display_name: display_name,
            ready: false,
            state: "Buffering...",
            latency: 0
        };
    },
    leave: function(user_id, video_id) {
        video_id = video_id || get_video_id_for_user_id(user_id);
        // Gotta make sure that the watchroom exists for the video_id as well
        if (video_id && watchrooms[video_id]) {
            delete watchrooms[video_id].viewers[user_id];
            if (!Object.keys(watchrooms[video_id].viewers).length) {
                delete watchrooms[video_id];
            }
        }
        return video_id;
    },
    update_user: function(user_id, video_id, state) {
        return Object.assign(
            watchrooms[
                video_id || get_video_id_for_user_id(user_id)
            ].viewers[user_id],
            state
        );
    },
    get_invoke_delay: function(video_id) {
        let max_latency = 0;
        for (const viewer_state of Object.values(watchrooms[video_id].viewers)) {
            if (viewer_state.latency > max_latency) {
                max_latency = viewer_state.latency;
            }
        }
        // Inflate by 2x to account for network fluctuations
        return max_latency * 2;
    },
    update_video: function(video_id, state, invoke_at, oninvoked) {
        //console.log("Before Update", watchrooms[video_id]);

        if (watchrooms[video_id].update_state_timeout) {
            return;
        }

        let update_state = state.invoked_by === "server";

        // Can only toggle paused state if value will change.
        if (Object.prototype.hasOwnProperty.call(state, "paused") && watchrooms[video_id].state.paused != state.paused) {
            update_state = true;
            if (state.paused) {
                // Video is currently playing...
                // The difference between when the video was last played and now is the duration played.
                let duration_played = invoke_at - watchrooms[video_id].play_invoked_at;
                // Increment the currentTime with the duration played. Subtract one
                // second in order to account for positive client clock offset
                watchrooms[video_id].state.currentTime += (duration_played / 1000) - 1;

                if (watchrooms[video_id].state.currentTime < 0) {
                    watchrooms[video_id].state.currentTime = 0;
                }
            } else {
                // Video is currently paused...
                // Set the last played time so that when the video is
                // paused the difference can be added to the currentTime
                watchrooms[video_id].play_invoked_at = invoke_at;
            }
            watchrooms[video_id].state.paused = state.paused;
        }

        // Cannot set new current time unless video is paused.
        if (Object.prototype.hasOwnProperty.call(state, "currentTime") && watchrooms[video_id].state.paused) {
            update_state = true;
            watchrooms[video_id].state.currentTime = Math.max(state.currentTime, 0);
        }

        if (update_state) {
            watchrooms[video_id].state.invoked_by = state.invoked_by;

            watchrooms[video_id].update_state_timeout = setTimeout(function() {
                watchrooms[video_id].update_state_timeout = undefined;
                oninvoked && oninvoked();
            }, Math.max(invoke_at - Date.now(), 0));

            //console.log("After Update", watchrooms[video_id]);

            return watchrooms[video_id].state;
        }
    },
    get: function(video_id) {
        return watchrooms[video_id];
    },
    get_all: function() {
        return watchrooms;
    }
};

function get_video_id_for_user_id(user_id) {
    for (const video_id of Object.keys(watchrooms)) {
        if (watchrooms[video_id].viewers[user_id]) {
            return video_id;
        }
    }
}