Browse Source

设备直播beta1

Melon 2 years ago
commit
10db72c62f

+ 0 - 0
readme.txt


+ 4 - 0
src/css/common.css

@@ -0,0 +1,4 @@
+html,
+body {
+    margin: 0;
+}

+ 24 - 0
src/css/device_live.css

@@ -0,0 +1,24 @@
+body {
+    background-color: #000;
+    overflow: hidden;
+}
+
+#app {
+    width: 100vw;
+    height: 100vh;
+}
+
+.main-view {
+    position: relative;
+    margin: 0 auto;
+    top: 50%;
+    transform: translateY(-50%);
+    overflow: hidden;
+}
+
+.sub-view {
+    position: fixed;
+    right: 0;
+    top: 0;
+    border: 1px solid #eee;
+}

+ 28 - 0
src/device_live.html

@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html lang="Zh-cn">
+
+<head>
+    <meta charset="UTF-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title></title>
+    <link rel="stylesheet" href="./css/common.css?v=202303281656">
+    <link rel="stylesheet" href="./css/device_live.css?v=202303281656">
+</head>
+
+<body>
+    <div id="app">
+        <div class="main-view">
+            <canvas id="main"></canvas>
+        </div>
+        <div class="sub-view"></div>
+    </div>
+    <script src="./lib/jquery/jquery.min.js?v=3.6.4"></script>
+    <script src="./lib/node_player/NodePlayer.min.js?v=0.11.8"></script>
+    <script src="./js/rpc.js?v=202303281656"></script>
+    <script src="./js/common.js?v=202303281656"></script>
+    <script src="./js/device_live/player.js?v=202303281656"></script>
+    <script src="./js/device_live/index.js?v=202303281656"></script>
+</body>
+
+</html>

+ 18 - 0
src/index.html

@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>FLYINSONO</title>
+</head>
+
+<body>
+    <div style="margin-top: 100px;text-align: center;">
+        <h3>FLYINSONO</h3>
+        <h1>Open Web Index</h1>
+    </div>
+</body>
+
+</html>

+ 15 - 0
src/js/common.js

@@ -0,0 +1,15 @@
+function getQueryString(name) {
+    const url_string = window.location.href;
+    const url = new URL(url_string);
+    return url.searchParams.get(name);
+}
+
+function getOpenUid() {
+    const key = 'fis_open_uid';
+    let uid = localStorage[key];
+    if (!uid) {
+        uid = crypto.randomUUID();
+        localStorage[key] = uid;
+    }
+    return uid;
+}

+ 187 - 0
src/js/device_live/index.js

@@ -0,0 +1,187 @@
+$(function () {
+    const code = getQueryString('code');
+    const openUid = getOpenUid();
+    NodePlayer.load(function () {
+        process();
+    });
+
+    async function process() {
+        const rst = await enterLive();
+        if (!rst) return;
+
+        // 心跳
+        let interval = parseInt(rst['ReportStateIntervalSeconds']);
+        let heartbeat = new LiveHeartbeat(interval);
+        heartbeat.start();
+
+        const useMerge = rst.MergedChannel && rst.VideoDeviceInfos.length > 1;
+        const mergeInfo = {
+            useMerge: useMerge,
+            width: rst.MergedVideoOutputWidth,
+            height: rst.MergedVideoOutputHeight,
+        };
+
+        function resizeMain() { }
+        function resizeCamera1() { }
+
+        const sources = rst.VideoDeviceInfos;
+        const mainIndex = sources.findIndex(x => x.VideoDeviceSourceType === 0);
+        let mainSizeInfo = null;
+        if (mainIndex > -1) {
+            const source = sources.splice(mainIndex, 1)[0].LiveData;
+            const width = source.Width;
+            const height = source.Height;
+            mainSizeInfo = { width: width, height: height };
+
+            function setSize() {
+                let winWHR = window.innerWidth / window.innerHeight;
+                let viewWHR = width / height;
+                let w, h;
+                if (winWHR > viewWHR) {
+                    // 高度适配
+                    h = window.innerHeight;
+                    w = h * viewWHR;
+                } else {
+                    // 宽度适配
+                    w = window.innerWidth;
+                    h = w / viewWHR;
+                }
+                if (mergeInfo.useMerge) {
+                    w = w / (width / mergeInfo.width);
+                    h = h / (height / mergeInfo.height);
+                }
+                $("#main").width(w);
+                $("#main").height(h);
+                $(".mian-view").width(w);
+                $(".mian-view").width(h);
+                return { w, h };
+            }
+            setSize();
+
+            // 主画面
+            let player = new LivePlayer("main", {
+                url: source.HttpPullUrl,
+                width: source.Width,
+                height: source.Height,
+                merge: mergeInfo,
+            });
+            player.play();
+
+            resizeMain = function () {
+                const { w, h } = setSize();
+                player.setSize(w, h)
+            }
+        }
+        if (sources.length > 0) {
+            const source = sources[0].LiveData;
+            const canvas = document.createElement('canvas');
+            canvas.id = "sub_" + 0;
+
+            function setSize() {
+                const ratio = source.Width / mainSizeInfo.width * .55;
+                let w = window.innerWidth * ratio;
+                if (w < 200) {
+                    w = 200;
+                }
+                const h = w / source.Width * source.Height;
+                canvas.width = w;
+                canvas.height = h;
+                return { w, h }
+            };
+            setSize();
+
+            const root = $(".sub-view");
+            root.append(canvas);
+
+            if (mergeInfo.useMerge) {
+                let player = new LiveSubPlayer(canvas.id, "main", {
+                    url: source.HttpPullUrl,
+                    width: source.Width,
+                    height: source.Height,
+                    merge: mergeInfo,
+                    main: mainSizeInfo,
+                });
+                player.play();
+                resizeCamera1 = function () {
+                    const { w, h } = setSize();
+                    // player.setSize(w, h)
+                }
+            } else {
+                let player = new LivePlayer(canvas.id, {
+                    url: source.HttpPullUrl,
+                    width: source.Width,
+                    height: source.Height,
+                });
+                player.play();
+                resizeCamera1 = function () {
+                    const { w, h } = setSize();
+                    player.setSize(w, h)
+                }
+            }
+        }
+
+        async function closePage() {
+            heartbeat && heartbeat.stop();
+            await exitLive();
+        }
+
+        window.onresize = function () {
+            resizeMain();
+            resizeCamera1();
+        }
+
+        window.onunload = function () {
+            closePage();
+        }
+
+        window.closePage = closePage;
+    }
+
+    async function enterLive() {
+        try {
+            const result = await rpc.call("IDeviceService", "JoinDeviceLiveRoomByShareAsync", { "ShareCode": code });
+            return result;
+        } catch (e) {
+            if (e instanceof JsonRpcError) {
+                //
+            }
+        }
+        return null;
+    }
+    function exitLive() {
+        try {
+            rpc.call("IDeviceService", "LeaveDeviceLiveRoomByShareAsync", { "DeviceCode": code, "ViewerUniqueId": openUid });
+        } catch (e) {
+            if (e instanceof JsonRpcError) {
+                //
+            }
+        }
+    }
+
+    function LiveHeartbeat(intervalSeconds) {
+        const intervalMillSeconds = 1000 * intervalSeconds - 300;
+        let running = false;
+        let pointer = 0;
+        this.start = function () {
+            running = true;
+            send();
+        }
+        this.stop = function () {
+            running = false;
+            pointer && clearTimeout(pointer);
+        }
+
+        async function send() {
+            if (!running) return;
+
+            try {
+                rpc.call("IDeviceService", "ReportLiveViewStateByShareAsync", { "DeviceCode": code, "ViewerUniqueId": openUid });
+            } catch (e) {
+                if (e instanceof JsonRpcError) {
+                    //
+                }
+            }
+            pointer = setTimeout(send, intervalMillSeconds);
+        }
+    }
+});

+ 157 - 0
src/js/device_live/player.js

@@ -0,0 +1,157 @@
+!(function () {
+    //v0.5.70版之后,在Android手机端推荐使用以下音频引擎
+    if (/(Android)/i.test(navigator.userAgent)) {
+        NodePlayer.activeAudioEngine(true);
+    }
+
+    function LivePlayer(elementId, options) {
+        const { url, } = options;
+        const eventCbMap = {};
+        const el = document.getElementById(elementId);
+        const player = new NodePlayer();
+        // 开启屏幕常亮
+        player.setKeepScreenOn();
+        // 绑定dom
+        player.setView(elementId);
+        // 设置最大缓冲时长,单位毫秒
+        player.setBufferTime(1000);
+        // 设置超时时长, 单位秒;回调timeout事件
+        player.setTimeout(10);
+        // 缩放模式,0 fill,1 contain,2 cover
+        player.setScaleMode(1);
+
+        player.on("videoInfo", (w, h, codec) => {
+            emit("videoInfo", { width: w, height: h });
+        });
+
+        player.on("timeout", () => {
+            emit("timeout");
+        });
+        player.on("start", () => {
+            emit("start");
+        });
+        player.on("stop", () => {
+            emit("stop");
+        });
+        player.on("error", (error) => {
+            // {code:404,msg:"not found"}
+            emit("error", error);
+        });
+
+        this.setSize = function (width, height) {
+            player.resizeView(width, height);
+        }
+
+        this.play = function () {
+            player.start(url);
+        }
+        this.stop = function () {
+            player.stop();
+        }
+        this.on = function (name, callback) {
+            let cbs = eventCbMap[name];
+            if (!cbs) {
+                cbs = [];
+                eventCbMap[name] = cbs;
+            }
+            cbs.push(callback);
+        }
+        this.off = function (name, callback) {
+            let cbs = eventCbMap[name];
+            if (!!cbs) {
+                let index = cbs.indexOf(callback);
+                if (index > -1) {
+                    cbs.splice(index, 1);
+                }
+            }
+        }
+        function emit(name, data) {
+            let cbs = eventCbMap[name];
+            if (!!cbs) {
+                let len = cbs.length;
+                for (let i = 0; i < len; i++) {
+                    cbs[i](data);
+                }
+            }
+        }
+    }
+    function LiveSubPlayer(elementId, sourceId, options) {
+        let isCopying = false;
+        const canvas = document.getElementById(elementId);
+        const sourceCanvas = document.getElementById(sourceId);
+
+        //TODO:这里仅摄像头1
+        const clipPosition = {
+            x: options.merge.width - options.width,
+            y: options.merge.height - options.height,
+        }
+
+        this.play = function () {
+            startCopying();
+        }
+        this.stop = function () {
+            stopCopying();
+        }
+
+        function startCopying() {
+            if (!isCopying) {
+                isCopying = true;
+
+                const sourceGL = sourceCanvas.getContext("webgl");
+                const scale = sourceGL.drawingBufferWidth / options.merge.width;
+
+                const block = {
+                    width: parseInt(options.width * scale),
+                    height: parseInt(options.height * scale),
+                    clipX: parseInt(clipPosition.x * scale),
+                    clipY: parseInt(clipPosition.y * scale),
+                };
+
+                canvas.width = block.width;
+                canvas.height = block.height;
+
+                const ctx = canvas.getContext("2d");
+
+
+                setInterval(() => copyVideoStream(sourceGL, ctx, block), 1000 / 20);
+            }
+        }
+
+        function stopCopying() {
+            if (isCopying) {
+                isCopying = false;
+                clearInterval();
+            }
+        }
+
+        function copyVideoStream(gl, ctx, block) {
+            const { width, height, clipX, clipY } = block;
+            const bufferPixels = new Uint8Array(width * height * 4);
+            // 添加逻辑检查当前帧是否为空
+            gl.readPixels(
+                clipX,
+                clipY,
+                width,
+                height,
+                gl.RGBA,
+                gl.UNSIGNED_BYTE,
+                bufferPixels
+            );
+            const isEmpty = bufferPixels.every((val) => val === 0);
+            if (isEmpty) {
+                return; // 如果帧为空,则直接返回,不进行后续处理
+            }
+
+            const imageData = ctx.createImageData(width, height);
+
+            imageData.data.set(bufferPixels);
+            ctx.putImageData(imageData, 0, 0);
+            // 沿着水平方向翻转并移动到底部
+            ctx.setTransform(1, 0, 0, -1, 0, height);
+            ctx.drawImage(canvas, 0, 0); // 绘制翻转后的画面
+            ctx.setTransform(1, 0, 0, 1, 0, 0); // 重置变换矩阵
+        }
+    }
+    window.LivePlayer = LivePlayer;
+    window.LiveSubPlayer = LiveSubPlayer;
+})();

+ 44 - 0
src/js/rpc.js

@@ -0,0 +1,44 @@
+!(function () {
+    function JsonRpcClient(options) {
+        const host = options.host;
+        this.call = async function (module, method, data) {
+            const req = packageReqObj(method, data);
+            const url = `${host}${module}`;
+            try {
+                const res = await $.post(url, JSON.stringify(req));
+                if (res == null) {
+                    return new JsonRpcError(-1, "Network Error");
+                }
+                if (!!res.error) {
+                    throw new JsonRpcError(res.error.code, res.error.message);
+                }
+                return res.result;
+            } catch (e) {
+                throw new JsonRpcError(-1, "Network Error");
+            }
+        }
+
+        function packageReqObj(method, data) {
+            return {
+                "jsonrpc": "2.0",
+                "id": crypto.randomUUID(),
+                "method": method,
+                "params": [data],
+            };
+        }
+    }
+    function JsonRpcError(code, message) {
+        Error.apply(this, arguments);
+
+        this.code = code;
+        this.message = message;
+    }
+    JsonRpcError.prototype = Object.create(Error.prototype);
+
+    window.JsonRpcClient = JsonRpcClient;
+    window.JsonRpcError = JsonRpcError;
+
+    window.rpc = new JsonRpcClient({
+        host: "http://192.168.6.117:8303/"
+    });
+})();

File diff suppressed because it is too large
+ 1 - 0
src/lib/jquery/jquery.min.js


File diff suppressed because it is too large
+ 0 - 0
src/lib/node_player/NodePlayer.min.js


BIN
src/lib/node_player/NodePlayer.min.wasm


Some files were not shown because too many files changed in this diff