Tuesday | 21 MAY 2024
[ previous ]
[ next ]

Running VLC in the Browser

Title:
Date: 2024-04-13
Tags:  

I've always wanted a media streaming service for my home but installing plex or jellyfin seemed like a big deal for something that I thought should be simple. I have directories of movies and tv shows that I want to watch on various computers and phones.

Background

My solution was to write a rust program that would serve the videos through nginx and then I would rely on the browser being able to play the video. The limitation was that I had to stick with mp4 with h264 video and aac audio. This meant that I had to convert quite a bit of my videos or download them specifically with those settings. This works and in my eyes a pretty good solution. I lose out on changing the audio and having subtitles but the lightweightness balanced it out.

VLC Wasm

Now there is a version of vlc that has been compiled to wasm that lets me play a wide variety of formats directly in the browser. My plan is to switch from using the browsers native video player to the vlc video player.

Below is the link to a working demo of the vlc wasm project. It lets you upload a video and play it directly in the browser which was in the truest sense of the word awesome.

https://videolabs.io/communication/vlcjs-demo/vlc.html

I then took a look at the gitlab:

https://code.videolan.org/jbk/vlc.js

Unfortunately, compiling directly from source proved to be difficult. I didn't spend too much time trying to figure out why it wasn't compiling. The demo exists on the web and so I knew I could take the binaries directly from the demo and use that for my own purposes.

I downloaded all the different pieces for the demo and set them up on my server and once I got it working, I began the process of hacking it into the shape of what I want. The goal was to get the demo to act more like the video tag. I wanted to be able to inclued a single script and add a couple html tags and have a video player just show up.

This was a bit of a process as the demo works slightly differently as it uses file uploads, the styling and svgs also added a ton of noise and so it was difficult to see what was required and what was extra. It was hard to see what part of vlc I need to call to actually start things.

I spent some time change the structure of things, renaming things, re-organizing the code into something that made sense to me and I think I have a pretty good handle on how things work now.

My Version of VLC.js

An example with all the source code needed to run VLC can be found at:

https://github.com/Krowemoh/vlc.js

I'll start off with an example of what I currently use:

<div>
    <canvas id="canvas" style="background-color:gray;"></canvas>
    <div>
        <button id="play">Play</button>
        <button id="pause">Stop</button>
        <meter id="seekbar" max="100" style="width: 500px" value="0"></meter>
        <meter id="volume" max="100" style="width: 100px" value="80"></meter>
    </div>
</div>

<script src="./lib/vlc/experimental.js"></script>
<script type="module">
    import { VLCPlayer } from "./lib/vlc/vlc.js";
    
    let video = {
       source: '/path/to/video.mkv'
       options:"--codec=webcodec --aout=emworklet_audio -vv --input-repeat=10000",
       size: {
          width: "700px",
          height: ""
       },
    };
    
    window.onload = async function () {
       await VLCPlayer(video);
    };
</script>

The canvas tag is required and it needs to have the id as canvas. Somewhere in the wasm the canvas id is used directly. There is a canvas attribute that can be modified which looks like it should let you specify the id to attach to but I couldn't get it to work.

I also have another div that contains the video player buttons. I would have liked to hook into the existing video player but I think using the meter and buttons is a nice compromise.

There are quite a few files that are needed to get vlc.js working:

audio-worklet-processor.js - This is what handles audio
experimental.js            -
experimental.wasm          - VLC compiled to wasm
experimental.worker.js     -  
libvlc.js                  - Bindings for the VLC wasm
vlc.js                     - Initialize VLC and Controls

These files are located under /lib/vlc. This pathing is also required. Inside experimental.js are hardcoded paths.

Getting back to my example, You create a video object that will contain the source, the options to pass to vlc and the size of the canvas you want to play the video in.

The final step is to call VLCPlayer which will initialize VLC and set up the click handlers for the controls. vlc.js is something that I added to simplify the code that I call directly.

This code should let you start VLC with any video and have it play directly in the browser.

vlc.js

Below is the full source of vlc.js. Most of the code is from the demo. I've cleaned things up and removed the parts that I didn't need. The core idea here looks to be that you load the VLC wasm and set it up. You can then fetch the video and attach it to VLC. You can then use VLC to play the video on the canvas and use VLC to start and stop the video.

import { MediaPlayer } from "./libvlc.js";

export async function VLCPlayer(video) {
    let canvas = document.getElementById("canvas");
    if (canvas === null) {
        console.error("No canvas with id 'canvas' found.");
        return;
    }
    
    window.Module = await initModule({ 
        vlc_access_file: {},
    });
    
    let media_player;
    
    let vlc_opts_array = video.options.split(' ');
    
    let vlc_opts_size = 0;
    for (let i in vlc_opts_array) {
        vlc_opts_size += vlc_opts_array[i].length + 1;
    }
    
    let buffer = Module._malloc(vlc_opts_size);
    let wrote_size = 0;
    for (let i in vlc_opts_array) {
        Module.writeAsciiToMemory(vlc_opts_array[i], buffer + wrote_size, false);
        wrote_size += vlc_opts_array[i].length + 1;
    }
    
    let vlc_argv = Module._malloc(vlc_opts_array.length * 4 + 4);
    let view_vlc_argv = new Uint32Array(
        Module.wasmMemory.buffer,
        vlc_argv,
        vlc_opts_array.length
    );
    
    wrote_size = 0;
    for (let i in vlc_opts_array) {
        view_vlc_argv[i] = buffer + wrote_size;
        wrote_size += vlc_opts_array[i].length + 1;
    }
    
    Module._wasm_libvlc_init(vlc_opts_array.length, vlc_argv);
    media_player = new MediaPlayer(Module, "emjsfile://1");
    media_player.set_volume(80);
    
    Module._set_global_media_player(media_player.media_player_ptr);
    
    window.media_player = media_player;
    
    window.update_overlay = function() { 
        let media_player = window.media_player;
        let position = media_player.get_position() * 100;
        let seekbar = document.getElementById('seekbar');
        seekbar.value = position;
    };
    
    initializeVLCControls(video);
}

function initializeVLCControls(video) { 
    let vtime = null;
    
    document.getElementById('play')?.addEventListener("click", async function() {
        if (vtime === null) {
            let r = await fetch(video.source);
            let blob = await r.blob()
            let file = new File([blob], "Video", { type: "video/mkv" });
            
            window.Module['vlc_access_file'][1] = file;
            window.files = [file];
            
            media_player.play();
            document.getElementById("canvas").style.width = video.size.width;
            document.getElementById("canvas").style.height = video.size.height;
            
        } else {
            media_player.set_position(vtime);
            media_player.play();
        }
    });
    
    document.getElementById('pause')?.addEventListener("click",function() {
        vtime = media_player.get_position();
        media_player.pause();
    });
    
    let seekbar = document.getElementById('seekbar');
    seekbar?.addEventListener("click", function(e) {
        let position = (e.offsetX / seekbar.clientWidth);
        vtime = position;
        seekbar.value = position * 100;
        media_player.set_position(position);
    });
    
    let volume = document.getElementById('volume');
    volume?.addEventListener("click", function(e) {
        let position = (e.offsetX / volume.clientWidth) * 100;
        volume.value = position;
        media_player.set_volume(position);
    });
}