FFmpeg WASM
Written: 27/7/2024
This is a WASM example on FFmpeg, compiled from source.
Select a media file from your device (nothing is uploaded to a server, it's all processed on your machine 😃)
[JS] Loading FFmpeg...
Result:
1

Source code

1. main.cpp
1
extern "C" {
2
    #include "libavformat/avformat.h"
3
}
4

5
#include <emscripten/bind.h>
6

7
emscripten::val init(const std::string inputPath) {
8
    emscripten::val result = emscripten::val::object();
9
    AVFormatContext* format_ctx = nullptr;
10
    if (avformat_open_input(&format_ctx, inputPath.c_str(), nullptr, nullptr) < 0) {
11
        result.set("exit", -1);
12
        return result;
13
    }
14

15
    if (avformat_find_stream_info(format_ctx, nullptr) < 0) {
16
        avformat_close_input(&format_ctx);
17
        result.set("exit", -1);
18
        return result;
19
    }
20

21
    
22
    result.set("format", format_ctx->iformat->name);
23
    result.set("duration", ((float)format_ctx->duration / (float)AV_TIME_BASE));
24
    result.set("nb_streams", format_ctx->nb_streams);
25

26
    emscripten::val streams = emscripten::val::array();
27
    for (unsigned int i = 0; i < format_ctx->nb_streams; i++) {
28
        AVStream *stream = format_ctx->streams[i];
29
        AVCodecParameters *codecpar = stream->codecpar;
30

31
        emscripten::val stream_info = emscripten::val::object();
32
        stream_info.set("index", i);
33
        stream_info.set("type", av_get_media_type_string(codecpar->codec_type));
34
        stream_info.set("codec", avcodec_get_name(codecpar->codec_id));
35

36
        emscripten::val metadata = emscripten::val::object();
37
        AVDictionaryEntry *tag = nullptr;
38
        while ((tag = av_dict_get(stream->metadata, "", tag, AV_DICT_IGNORE_SUFFIX))) {
39
            metadata.set(std::string(tag->key), std::string(tag->value));
40
        }
41

42

43
        stream_info.set("metadata", metadata);
44

45
        if (codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
46
            stream_info.set("width", codecpar->width);
47
            stream_info.set("height", codecpar->height);
48
            stream_info.set("fps", av_q2d(stream->avg_frame_rate));
49
        } else if (codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
50
            stream_info.set("sample_rate", codecpar->sample_rate);
51
            stream_info.set("channels", codecpar->channels);
52
        }
53

54
        streams.call<void>("push", stream_info);
55
    }
56

57
    result.set("streams", streams);
58
    result.set("exit", 0);
59
    avformat_close_input(&format_ctx);
60
    return result;
61
}
62

63
EMSCRIPTEN_BINDINGS(alhaddar) {
64
    emscripten::function("init", &init);
65
}
2. page.svelte (this page)
1
<script lang="ts">
2
	import PageContainer from "$lib/components/PageContainer.svelte";
3
	import "highlight.js/styles/github-dark.css";
4
	import { onMount } from "svelte";
5
    import { Highlight, HighlightSvelte, LineNumbers } from "svelte-highlight";
6
    import { cpp, javascript, json } from "svelte-highlight/languages/index.js";
7

8
	export let data;
9

10
	let loaded = false;
11
	let worker: Worker;
12
	let stdout: string = "[JS] Loading FFmpeg...";
13
	let processResponse: string = '';
14

15
	onMount(() => {
16
		worker = new Worker("./ffmpeg-wasm/worker.js", {
17
			name: "worker.js",
18
		});
19

20
		worker.addEventListener("message", (msg) => {
21
			if (msg.data.type === "stdout") {
22
				stdout += msg.data.payload;
23
			}
24
			if (msg.data.type === "loaded") {
25
				loaded = msg.data.payload;
26
			}
27
			if (msg.data.type === "processResponse") {
28
				processResponse = msg.data.payload
29
			}
30
		});
31
	});
32

33
	async function process(e: Event) {
34
		e.preventDefault();
35
		const fileInput = document.getElementById('inputFile') as HTMLInputElement;
36
		if (!fileInput.files) return;
37
		stdout += "[JS] Sending file from JS to WASM\n";
38
		const file = fileInput.files[0];
39
            if (!file) {
40
                console.log("No file selected");
41
                return;
42
            }
43

44
		worker.postMessage({
45
			type: "process",
46
			payload: { name: file.name, data: file }
47
		})
48
	}
49
</script>
50

51
<svelte:head>
52
	<style>
53
		#stdout {
54
			background-color: black;
55
			color: green;
56
			min-height: 200px;
57
			padding: 10px;
58
			font-weight: bold;
59
			font-family: monospace;
60
			white-space: pre-wrap;
61
		}
62
		#file-form {
63
			display: flex;
64
			flex-shrink: 1;
65
			flex-direction: column;
66
			gap: 10px;
67
		}
68
	</style>
69
</svelte:head>
70

71
<PageContainer>
72
	<div>
73
		<div class="text-2xl font-bold text-secondary">
74
			{data.title}
75
		</div>
76
		<div class="text-sm font-bold text-primary">
77
			Written: {data.written}
78
		</div>
79

80
		<div>
81
			This is a WASM example on FFmpeg, compiled from source.
82
		</div>
83

84
		<form id="file-form" on:submit={process}>
85
			<h6 class="text">Select a media file from your device (nothing is uploaded to a server, it's all processed on your machine 😃)</h6>
86
			
87
			<input id="inputFile" type="file" />
88
			<button 
89
				disabled={!loaded} 
90
				class="btn btn-primary" 
91
				type="submit"
92
			>
93
				Get streams metadata
94
			</button>
95
			<div id="stdout">{stdout}</div>
96
		</form>
97

98
		<div class="flex flex-col gap-2 mt-5">
99
			Result:
100
			<Highlight language={json} code={processResponse} let:highlighted>
101
				<LineNumbers {highlighted} />
102
			</Highlight>
103

104
			<h1 class="text-primary text-2xl">Source code</h1>
105

106
			1. main.cpp
107
			<Highlight language={cpp} code={data["main.cpp"]} let:highlighted>
108
				<LineNumbers {highlighted} />
109
			</Highlight>
110

111
			2. page.svelte (this page)
112
			<HighlightSvelte code={data["svelteSource"]} let:highlighted>
113
				<LineNumbers {highlighted} />
114
			</HighlightSvelte>
115

116
			3. worker.js
117
			<Highlight language={javascript} code={data.workerSource} let:highlighted>
118
				<LineNumbers {highlighted} />
119
			</Highlight >
120

121
			4. FFmpeg build script using emscripten
122
			<Highlight language={javascript} code={data.ffmpegBuild} let:highlighted>
123
				<LineNumbers {highlighted} />
124
			</Highlight >
125

126
			5. WASM build script
127
			<Highlight language={javascript} code={data.wasmBuild} let:highlighted>
128
				<LineNumbers {highlighted} />
129
			</Highlight >
130
		</div>
131

132
		References:
133
		<ul class="list-disc p-[revert] pb-2">
134
			<li class="list-item"><a class="link-secondary" href="https://jeromewu.github.io/build-ffmpeg-webassembly-version-part-2-compile-with-emscripten">https://jeromewu.github.io/build-ffmpeg-webassembly-version-part-2-compile-with-emscripten</a></li>
135
			<li><a class="link-secondary" href="https://github.com/ffmpegwasm/ffmpeg.wasm">https://github.com/ffmpegwasm/ffmpeg.wasm</a></li>
136
			<li><a class="link-secondary" href="https://github.com/sc0ty/subsync">https://github.com/sc0ty/subsync</a></li>
137
		</ul>
138
	</div>
139
</PageContainer>
140

3. worker.js
1
__filename = "ffmpeg.js"
2
importScripts("ffmpeg.js");
3

4
let ffmpegModule;
5

6
createFFmpeg({
7
    noInitialRun: true,
8
    locateFile: (path) => {
9
        return path;
10
    },
11
    print: (str) => {
12
        self.postMessage({
13
            type: "stdout",
14
            payload: `[WASM] ${str}\n`,
15
        });
16
    },
17
})
18
    .then((lib) => {
19
        ffmpegModule = lib;
20
        self.postMessage({
21
            type: "stdout",
22
            payload: "\n[JS] Loaded\n",
23
        });
24
        self.postMessage({
25
            type: "loaded",
26
            payload: true,
27
        });
28
        
29
    })
30
    .catch(console.log);
31

32
self.onmessage = (msg) => {
33
    if (msg.data.type === "process") {
34
        const {payload: file} = msg.data;
35
        const mountPoint = '/root'
36
        
37
        if (!ffmpegModule.FS.analyzePath(mountPoint).exists) {
38
            ffmpegModule.FS.mkdir(mountPoint);
39
        }
40
        ffmpegModule.FS.mount(ffmpegModule.FS.filesystems.WORKERFS, {
41
            blobs: [file]
42
        }, mountPoint);
43

44
        const filePath = `${mountPoint}/` + file.name;
45
        const begin = performance.now()
46
        const result = ffmpegModule.init(filePath)
47
        const end = performance.now();
48
        self.postMessage({
49
            type: "stdout",
50
            payload: `[JS] init() exited with code ${result.exit}. Time: ${(end-begin).toFixed(3)}ms\n`,
51
        });
52
        self.postMessage({
53
            type: "processResponse",
54
            payload: JSON.stringify(result, null , 4),
55
        });
56
        console.log(result);
57
        ffmpegModule.FS.unmount(mountPoint)
58
    }
59
};
4. FFmpeg build script using emscripten
1
#!/bin/bash -x
2

3
emcc -v
4

5
FLAGS=(
6
    --target-os=none
7
    --arch=x86_32
8
    --enable-cross-compile
9
    --disable-asm
10
    --disable-stripping
11
    --disable-programs
12
    --disable-doc
13
    --disable-debug
14
    --disable-runtime-cpudetect
15
    --disable-autodetect
16

17
    --nm=emnm
18
    --ar=emar
19
    --ranlib=emranlib
20
    --cc=emcc
21
    --cxx=em++
22
    --objcc=emcc
23
    --dep-cc=emcc
24
)
25

26
cd FFmpeg
27
emconfigure ./configure "${FLAGS[@]}"
28
make -B -j
29

5. WASM build script
1
mkdir -p ffmpeg-wasm
2

3
em++ \
4
    -I./FFmpeg/ \
5
    -L./FFmpeg/libavcodec \
6
    -L./FFmpeg/libavdevice \
7
    -L./FFmpeg/libavfilter \
8
    -L./FFmpeg/libavformat \
9
    -L./FFmpeg/libavutil \
10
    -L./FFmpeg/libswresample \
11
    -L./FFmpeg/libswscale \
12
    -lavcodec  -lavdevice -lavfilter -lavformat -lavutil -lswresample -lswscale  \
13
    -lembind -lworkerfs.js \
14
    -sINITIAL_MEMORY=1024MB \
15
    -sPTHREAD_POOL_SIZE=32 \
16
    -sEXPORTED_RUNTIME_METHODS=FS \
17
    -sMODULARIZE -sEXPORT_NAME="createFFmpeg"\
18
    -o ./ffmpeg-wasm/ffmpeg.js \
19
    ./src/main.cpp
References:
$ git rev-parse --short HEAD
1597778