Перейти к содержанию

Canvas Streaming

Описание

Данный пример показывает, как захватить видео с вебкамеры, отрисовать его на HTML5 канвасе с возможностью зеркалирования и опублоиковать на сервер.

На скриншоте ниже видео поток опубликован с канваса размером 320x240 с использованием метода requestAnimationFrame для обновления канваса и зеркалированием изображения

Код примера

Код данного примера находится на WCS-сервере по следующему пути:

/usr/local/FlashphonerWebCallServer/client2/examples/demo/streaming/canvas_streaming

  • canvas_streaming.css - файл стилей
  • canvas_streaming.html - HTML страница примера
  • canvas_streaming.js - скрипт, обеспечивающий работу примера

Тестировать данный пример можно по следующему адресу:

https://host:8888/client2/examples/demo/streaming/canvas_streaming/canvas_streaming.html

Здесь host - адрес WCS-сервера.

Работа с кодом примера

Для разбора кода возьмем версию файла canvas_streaming.js с хешем 485b3fb, которая находится здесь и доступна для скачивания в соответствующей сборке 2.0.259.

1. Инициализация API

Flashphoner.init() code

const init_page = function() {
    //init api
    try {
        Flashphoner.init();
    } catch (e) {
        setText("notifyFlash", "Your browser doesn't support WebRTC technology needed for this example");
        return;
    }

    ...
}

2. Создание контейнера для видео элемента

code

Видео и аудио дорожки захватываются с HTML5 видео элемента, который находится вне DOM HTML страницы и, следовательно, не отображается. Таким образом, контейнер для этого элемента также должен быть вне DOM.

const init_page = function() {
    ...

    localVideo = document.createElement("localVideo");
    ...
}

3. Подключение к серверу

Flashphoner.createSession() code

const connect = function() {
    let url = getValue('urlServer');

    //create session
    console.log("Create new session with url " + url);
    Flashphoner.createSession({urlServer: url}).on(SESSION_STATUS.ESTABLISHED, function(session){
        ...
    }).on(SESSION_STATUS.DISCONNECTED, function(){
        ...
    }).on(SESSION_STATUS.FAILED, function(){
        ...
    });
}

4. Получение от сервера события, подтверждающего успешное соединение

SESSION_STATUS.ESTABLISHED code

const connect = function() {
    let url = getValue('urlServer');

    //create session
    console.log("Create new session with url " + url);
    Flashphoner.createSession({urlServer: url}).on(SESSION_STATUS.ESTABLISHED, function (session) {
        currentSession = session;
        setStatus("connectStatus", session.status());
        startStreaming();
    }).on(SESSION_STATUS.DISCONNECTED, function () {
        ...
    }).on(SESSION_STATUS.FAILED, function () {
        ...
    });
}

5. Публикация потока с канваса

Session.createStream(), Stream.publish() code

Методу createStream() передаются следующие параметры:

  • streamName - имя потока
  • localVideo - контейнер для HTML5 видео элемента
  • constraints.audio: false - аудио дорожки добавляются в поток с канваса
  • constraints.video: false - видео дорожки добавляются к потоку с канваса
  • constraints.customStream - медиа поток с канваса
const startStreaming = function() {
    let session = currentSession;
    let streamName = getValue("urlServer").split('/')[3];
    let canvasStream = createCanvasStream();

    session.createStream({
        name: streamName,
        display: localVideo,
        constraints: {
            audio: false,
            video: false,
            customStream: canvasStream
        }
    }).on(STREAM_STATUS.PUBLISHING, function (stream) {
        ...
    }).on(STREAM_STATUS.UNPUBLISHED, function () {
        ...
    }).on(STREAM_STATUS.FAILED, function () {
        ...
    }).publish();
}

6. Получение события, подтверждающего успешную публикацию

STREAM_STATUS.PUBLISHING code

По данному событию запускается проигрывание опубликолванного потока с сервера

const startStreaming = function() {
    ...
    session.createStream({
        ...
    }).on(STREAM_STATUS.PUBLISHING, function (stream) {
        setStatus("publishStatus", STREAM_STATUS.PUBLISHING);
        playStream();
        onPublishing(stream);
    }).on(STREAM_STATUS.UNPUBLISHED, function () {
        ...
    }).on(STREAM_STATUS.FAILED, function () {
        ...
    }).publish();
}

7. Проигрывание потока с сервера

Session.createStream(), Stream.play() code

Методу createStream() передаются следующие параметры:

  • streamName - имя потока
  • remoteVideo - контейнер для HTML5 видео элемента
  • constraints - ограничения для проигрывания потока

Также, размер элемента для отображения устанавливается равным размеру канваса

const playStream = function() {
    let session = currentSession;
    let streamName = getValue("urlServer").split('/')[3];
    let width = getValue("width");
    let height = getValue("height");

    setDisplaySize(remoteVideo.parentNode, width, height);

    session.createStream({
        name: streamName,
        display: remoteVideo,
        constraints: {
            audio: !Browser.isiOS(),
            video: true
        }
    }).on(STREAM_STATUS.PENDING, function (stream) {
        ...
    }).on(STREAM_STATUS.PLAYING, function (stream) {
        ...
    }).on(STREAM_STATUS.STOPPED, function () {
        ...
    }).on(STREAM_STATUS.FAILED, function (stream) {
        ...
    }).play();
}

8. Получение события, подтверждающего успешное проигрывание

STREAM_STATUS.PLAYING code

const playStream = function() {
    ...
    session.createStream({
        ...
    }).on(STREAM_STATUS.PENDING, function (stream) {
        ...
    }).on(STREAM_STATUS.PLAYING, function (stream) {
        setStatus("playStatus", stream.status());
        onPlaying(stream);
    }).on(STREAM_STATUS.STOPPED, function () {
        ...
    }).on(STREAM_STATUS.FAILED, function (stream) {
        ...
    }).play();
}

9. Остановка проигрывания

Stream.stop() code

const stopBtnClick = function() {
    ...
    if (previewStream != null) {
        previewStream.stop();
        previewStream = null;
    }
}

10. Получение события, подтверждающего остановку проигрывания

STREAM_STATUS.STOPPED code

const playStream = function() {
    ...
    session.createStream({
        ...
    }).on(STREAM_STATUS.PENDING, function (stream) {
        ...
    }).on(STREAM_STATUS.PLAYING, function (stream) {
        ...
    }).on(STREAM_STATUS.STOPPED, function () {
        setStatus("playStatus", STREAM_STATUS.STOPPED);
        onStopped();
    }).on(STREAM_STATUS.FAILED, function (stream) {
        ...
    }).play();
}

11. Остановка публикации после проигрывания

Stream.stop() code

const onStopped = function() {
    ...
    if (publishStream != null && publishStream.published()) {
        publishStream.stop();
    }
}

12. Получение события, подтверждающего остановку публикации

STREAM_STATUS.UNPUBLISHED code

const startStreaming = function() {
    ...
    session.createStream({
        ...
    }).on(STREAM_STATUS.PUBLISHING, function (stream) {
        ...
    }).on(STREAM_STATUS.UNPUBLISHED, function () {
        setStatus("publishStatus", STREAM_STATUS.UNPUBLISHED);
        onUnpublished();
        disconnect();
    }).on(STREAM_STATUS.FAILED, function () {
        ...
    }).publish();
}

13. Остановка потока с канваса и уничтожение канваса

code

const onUnpublished = function() {
    publishStream = null;
    stopCanvasStream();
}

14. Отключение от сервера

Session.disconnect() code

const disconnect = function() {
    if (currentSession) {
        currentSession.disconnect();
    }
}

15. Получение события, подтверждащего успешное отключение

SESSION_STATUS.DISCONNECTED code

const connect = function() {
    let url = getValue('urlServer');

    //create session
    console.log("Create new session with url " + url);
    Flashphoner.createSession({urlServer: url}).on(SESSION_STATUS.ESTABLISHED, function (session) {
        ...
    }).on(SESSION_STATUS.DISCONNECTED, function () {
        setStatus("connectStatus", SESSION_STATUS.DISCONNECTED);
        onDisconnected();
    }).on(SESSION_STATUS.FAILED, function () {
        ...
    });
}

16. Инициализация канваса и видео элемента

code

const createCanvasStream = function() {
    let type = getCheckbox("webGl") ? CANVAS_TYPE.CANVAS_WEBGL : CANVAS_TYPE.CANVAS_2D;
    let width = getValue("width");
    let height = getValue("height");
    let constraints = {};
    canvas = Canvas("canvasContainer", width, height, type,
        getCheckbox("mirror"), getCheckbox("useAnimFrame"));
    mockVideo = Video(canvas);
    if (!getCheckbox("sendVideo")) {
        constraints.video = false;
    } else {
        constraints.video = {
            width: width,
            height: height
        };
    }
    constraints.audio = getCheckbox("sendAudio");
    mockVideo.start(constraints);
    return canvas.canvasStream();
}

17. Остановка захвата видео и уничтожение канваса

code

const stopCanvasStream = function() {
    if (mockVideo) {
        mockVideo.stop();
    }
    if (canvas) {
        canvas.close();
    }
}

18. Работа с канвасом

code

const Canvas = function(parentId, width, height, type, mirror, useRequestAnimationFrame) {
    const canvasObject = {
        canvas: null,
        useRequestAnimationFrame: false,
        context: null,
        stream: null,
        init: function(parentId, width, height, type, mirror, useRequestAnimationFrame) {
            ...
        },
        close: function() {
            ...
        },
        drawFrame: function(source) {
            ...
        },
        loop: function(video) {
            i...
        },
        canvasStream: function() {
            return stream;
        }
    };
    canvasObject.init(parentId, width, height, type, mirror, useRequestAnimationFrame);
    return canvasObject;
}

18.1 Создание канваса

code

const Canvas = function(parentId, width, height, type, mirror, useRequestAnimationFrame) {
    const canvasObject = {
        ...
        init: function(parentId, width, height, type, mirror, useRequestAnimationFrame) {
            let parent = document.getElementById(parentId);
            if (parent) {
                canvasObject.canvas = document.createElement("canvas");
                canvasObject.canvas.width = width;
                canvasObject.canvas.height = height;
                parent.appendChild(canvasObject.canvas);
                setDisplaySize(parent, width, height);
                canvasObject.mirror = mirror;
                canvasObject.useRequestAnimationFrame = useRequestAnimationFrame;
                if (type === CANVAS_TYPE.CANVAS_2D) {
                    canvasObject.context = Canvas2d(canvasObject.canvas, mirror);
                } else if (type === CANVAS_TYPE.CANVAS_WEBGL) {
                    canvasObject.context = CanvasWebGl(canvasObject.canvas, mirror);
                }
                stream = canvasObject.canvas.captureStream(30);
            }
        },
        close: function() {
            ...
        },
        drawFrame: function(source) {
            ...
        },
        loop: function(video) {
            ...
        },
        canvasStream: function() {
            ...
        }
    };
    canvasObject.init(parentId, width, height, type, mirror, useRequestAnimationFrame);
    return canvasObject;
}

18.2 Уничтожение канваса

code

const Canvas = function(parentId, width, height, type, mirror, useRequestAnimationFrame) {
    const canvasObject = {
        ...
        init: function(parentId, width, height, type, mirror, useRequestAnimationFrame) {
            ...
        },
        close: function() {
            if (canvasObject.canvas) {
                canvasObject.canvas.parentNode.style.display = "none";
                canvasObject.canvas.remove();
                canvasObject.canvas = null;
                canvasObject.stream = null;
            }
            canvasObject.useRequestAnimationFrame = false;
            canvasObject.context = null;
        },
        drawFrame: function(source) {
            ...
        },
        loop: function(video) {
            ...
        },
        canvasStream: function() {
            ...
        }
    };
    canvasObject.init(parentId, width, height, type, mirror, useRequestAnimationFrame);
    return canvasObject;
}

18.3 Отрисовка кадра на канвасе

code

const Canvas = function(parentId, width, height, type, mirror, useRequestAnimationFrame) {
    const canvasObject = {
        ...
        init: function(parentId, width, height, type, mirror, useRequestAnimationFrame) {
            ...
        },
        close: function() {
            ...
        },
        drawFrame: function(source) {
            if (source && canvasObject.context) {
                canvasObject.context.drawFrame(source);
            }
        },
        loop: function(video) {
            ...
        },
        canvasStream: function() {
            ...
        }
    };
    canvasObject.init(parentId, width, height, type, mirror, useRequestAnimationFrame);
    return canvasObject;
}

18.4 Метод для циклической перерисовки канваса

code

const Canvas = function(parentId, width, height, type, mirror, useRequestAnimationFrame) {
    const canvasObject = {
        ...
        init: function(parentId, width, height, type, mirror, useRequestAnimationFrame) {
            ...
        },
        close: function() {
            ...
        },
        drawFrame: function(source) {
            ...
        },
        loop: function(video) {
            if (!video.paused && !video.ended) {
                canvasObject.drawFrame(video);
                if (canvasObject.useRequestAnimationFrame) {
                    requestAnimationFrame(() => {
                        canvasObject.loop(video);
                    });
                } else {
                    setTimeout(() => {
                        canvasObject.loop(video);
                    }, 1000 / 30); // drawing at 30fps
                }
            }
        },
        canvasStream: function() {
            ...
        }
    };
    canvasObject.init(parentId, width, height, type, mirror, useRequestAnimationFrame);
    return canvasObject;
}

18.5 Зеркалирование изображения на 2d канвасе

code

const Canvas2d = function(canvas, mirror) {
    const canvas2d = {
        canvas: null,
        api: null,
        init: function(canvas, mirror) {
            if (canvas) {
                canvas2d.canvas = canvas;
                let context = canvas2d.canvas.getContext(CANVAS_TYPE.CANVAS_2D);
                if (mirror) {
                    context.translate(canvas2d.canvas.width, 0);
                    context.scale(-1, 1);
                    context.save();
                }
                canvas2d.api = {
                    context: context
                }
            }
        },
        close: function() {
            ...
        },
        drawFrame: function(source) {
            ...
        }
    };
    canvas2d.init(canvas, mirror);
    return canvas2d;
}

18.6 Отрисовка кадра на 2d канвасе

code

const Canvas2d = function(canvas, mirror) {
    const canvas2d = {
        canvas: null,
        api: null,
        init: function(canvas, mirror) {
            ...
        },
        close: function() {
            ...
        },
        drawFrame: function(source) {
            if (source && canvas2d.api && canvas2d.api.context) {
                canvas2d.api.context.drawImage(source, 0, 0);
            }
        }
    };
    canvas2d.init(canvas, mirror);
    return canvas2d;
}

18.7 Mirroring WebGL canvas

code

const CanvasWebGl = function(canvas, mirror) {
    const canvasWebGl = {
        canvas: null,
        api: null,
        init: function(canvas, mirror) {
            if (canvas) {
                canvasWebGl.canvas = canvas;
                let context = canvasWebGl.canvas.getContext(CANVAS_TYPE.CANVAS_WEBGL);
                let vertexShaderSource = ...;
                if (mirror) {
                    vertexShaderSource = `
                  attribute vec2 a_position;
                  attribute vec2 a_texCoord;
                  varying vec2 v_texCoord;
                  void main() {
                    gl_Position = vec4(a_position, 0, 1);
                    v_texCoord = vec2(1.0 - a_texCoord.x, a_texCoord.y); // X axis mirroring
                  }
                `;
                }

                ...

                const texture = context.createTexture();
                ...
                context.pixelStorei(context.UNPACK_FLIP_Y_WEBGL, true);

                ...
                canvasWebGl.api = {
                    context: context,
                    program: program,
                    positionBuffer: positionBuffer,
                    posLoc: posLoc,
                    texCoordBuffer: texCoordBuffer,
                    texLoc: texLoc,
                    texture: texture,
                    uTexLoc: uTexLoc
                };
            }
        },
        close: function() {
            ...
        },
        drawFrame: function(source) {
            ...
        }
    };
    canvasWebGl.init(canvas, mirror);
    return canvasWebGl;
}

18.8 Отрисовка кадра на WebGL канвасе

code

const CanvasWebGl = function(canvas, mirror) {
    const canvasWebGl = {
        canvas: null,
        api: null,
        init: function(canvas, mirror) {
            ...
        },
        close: function() {
            ...
        },
        drawFrame: function(source) {
            if (source && canvasWebGl.api && canvasWebGl.api.context) {
                let context = canvasWebGl.api.context;
                context.viewport(0, 0, canvasWebGl.canvas.width, canvasWebGl.canvas.height);
                context.clear(context.COLOR_BUFFER_BIT);

                context.useProgram(canvasWebGl.api.program);

                // Position
                context.bindBuffer(context.ARRAY_BUFFER, canvasWebGl.api.positionBuffer);
                context.enableVertexAttribArray(canvasWebGl.api.posLoc);
                context.vertexAttribPointer(canvasWebGl.api.posLoc, 2, context.FLOAT, false, 0, 0);

                // Texture coordinates
                context.bindBuffer(context.ARRAY_BUFFER, canvasWebGl.api.texCoordBuffer);
                context.enableVertexAttribArray(canvasWebGl.api.texLoc);
                context.vertexAttribPointer(canvasWebGl.api.texLoc, 2, context.FLOAT, false, 0, 0);

                // Renew texture from source
                context.bindTexture(context.TEXTURE_2D, canvasWebGl.api.texture);
                context.texImage2D(
                    context.TEXTURE_2D, 0, context.RGBA, context.RGBA,
                    context.UNSIGNED_BYTE, source
                );
                context.uniform1i(canvasWebGl.api.uTexLoc, 0);

                context.drawArrays(context.TRIANGLES, 0, 6);
            }
        }
    };
    canvasWebGl.init(canvas, mirror);
    return canvasWebGl;
}

19. Работа с элементом для захвата видео с камеры

code

const Video = function(canvas) {
    const videoObject = {
        canvas: null,
        video: null,
        init: function(canvas) {
            ...
        },
        start: function(constraints) {
            ...
        },
        stop: function() {
            ...
        }
    };
    videoObject.init(canvas);
    return videoObject;
}

19.1 Создание видео элемента

code

const Video = function(canvas) {
    const videoObject = {
        canvas: null,
        video: null,
        init: function(canvas) {
            videoObject.canvas = canvas;
            videoObject.video = document.createElement("video");
            videoObject.video.setAttribute("playsinline", "");
            videoObject.video.setAttribute("webkit-playsinline", "");
            videoObject.video.muted = true;
            videoObject.video.addEventListener("play", () => {
                videoObject.canvas.loop(videoObject.video);
            }, 0);
        },
        start: function(constraints) {
            ...
        },
        stop: function() {
            ...
        }
    };
    videoObject.init(canvas);
    return videoObject;
}

19.2 Запуск захвата видео

code

const Video = function(canvas) {
    const videoObject = {
        canvas: null,
        video: null,
        init: function(canvas) {
            ...
        },
        start: function(constraints) {
            let hasVideo = false;
            let hasAudio = false;
            let canvasStream = videoObject.canvas.canvasStream();
            if (constraints.video) {
                hasVideo = true;
            }
            if (constraints.audio) {
                hasAudio = true;
            }
            navigator.mediaDevices.getUserMedia(constraints)
                .then((stream) => {
                    videoObject.video.srcObject = stream;
                    videoObject.video.onloadedmetadata = () => {
                        if (!hasVideo) {
                            canvasStream.removeTrack(canvasStream.getVideoTracks()[0]);
                        }
                        if (hasAudio) {
                            videoObject.video.muted = false;
                            try {
                                let audioContext = new (window.AudioContext || window.webkitAudioContext)();
                                let source = audioContext.createMediaElementSource(videoObject.video);
                                let destination = audioContext.createMediaStreamDestination();
                                source.connect(destination);
                                canvasStream.addTrack(destination.stream.getAudioTracks()[0]);
                            } catch (e) {
                                console.warn("Failed to create audio context");
                            }
                        }
                    };
                    videoObject.video.play();
                });
        },
        stop: function() {
            ...
        }
    };
    videoObject.init(canvas);
    return videoObject;
}

19.3 Остановка захвата видео

code

const Video = function(canvas) {
    const videoObject = {
        canvas: null,
        video: null,
        init: function(canvas) {
            ...
        },
        start: function(constraints) {
            ...
        },
        stop: function() {
            if (videoObject.video) {
                videoObject.video.pause();
                videoObject.video.removeEventListener('play', null);
                let tracks = videoObject.video.srcObject.getTracks();
                for (let i = 0; i < tracks.length; i++) {
                    tracks[i].stop();
                }
                videoObject.video.srcObject = null;
                videoObject.video = null;
                videoObject.canvas = null;
            }
        }
    };
    videoObject.init(canvas);
    return videoObject;
}