WebAssembly 就是運行在 Web 平臺上的 Assembly。Assembly 是指匯編代碼,是直接操作 CPU 的指令代碼,比如 x86 指令集上的匯編代碼有指令集、寄存器、棧等等設計,CPU 根據匯編代碼的指導進行運算。匯編代碼相當于 CPU 執行的機器碼能夠轉換成的人類適合讀的一種語言。
Wasm的技術優勢: 性能高效:WASM采用二進制編碼,在程序執行過程中的性能優越; 存儲成本低:相對于文本格式,二進制編碼的文本占用的存儲空間更小; 多語言支持:用戶可以使用 C/C++/RUST/Go等多種語言編寫智能合約并編譯成WASM格式的字節碼;
(1)CSDN上的下載地址
下載地址: https://download.csdn.net/download/xiaolong1126626497/82868215
(2)GitHub倉庫下載地址
https://github.com/wang-bin/avbuild
https://sourceforge.net/projects/avbuild/files/
https://sourceforge.net/projects/avbuild/files/wasm/
(3)這里有編譯好的ffmpeg.wasm文件,前端JS可以直接調用完成視頻轉碼等功能 https://github.com/ffmpegwasm/ffmpeg.wasm
const fs = require('fs');
const { createFFmpeg, fetchFile } = require('@ffmpeg/ffmpeg');
const ffmpeg = createFFmpeg({ log: true });
(async () => {
await ffmpeg.load();
ffmpeg.FS('writeFile', 'test.avi', await fetchFile('./test.avi'));
await ffmpeg.run('-i', 'test.avi', 'test.mp4');
await fs.promises.writeFile('./test.mp4', ffmpeg.FS('readFile', 'test.mp4'));
process.exit(0);
})();
(4)ffmpeg編譯wasm文件的源碼,可以自行編譯wasm文件: https://github.com/ffmpegwasm/ffmpeg.wasm-core
下面只是編寫了一個打印版本號的函數,用于測試ffmpeg的庫和相關函數是否可以正常調用。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/imgutils.h>
#include <libswresample/swresample.h>
#include <emscripten/emscripten.h>
#include <libavcodec/version.h>
//獲取版本號
void print_version()
{
unsigned codecVer = avcodec_version();
int ver_major, ver_minor, ver_micro;
ver_major = (codecVer >> 16) & 0xff;
ver_minor = (codecVer >> 8) & 0xff;
ver_micro = (codecVer) & 0xff;
printf("當前ffmpeg的版本:avcodec version is: %d=%d.%d.%d\n", codecVer, ver_major, ver_minor, ver_micro);
}
emcc wasm_ffmpeg/wasm_ffmpeg.c ffmpeg-4.4-wasm/lib/libavformat.a ffmpeg-4.4-wasm/lib/libavcodec.a ffmpeg-4.4-wasm/lib/libswresample.a ffmpeg-4.4-wasm/lib/libavutil.a -I "ffmpeg-4.4-wasm/include" -s EXPORTED_FUNCTIONS="['_malloc','_free','ccall','allocate','UTF8ToString','_print_version']" -s WASM=1 -s ASSERTIONS=0 -s TOTAL_MEMORY=167772160 -s ALLOW_MEMORY_GROWTH=1 -o out/ffmpeg_decoder.js
編譯成功后生成的wasm和js文件:
編寫HTML文件調用js文件里的接口。
<!doctype html>
<html lang="en-us">
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>js調用c語言函數示例</title>
</head>
<body>
<script type='text/javascript'>
function run1()
{
_print_version();
}
</script>
<input type="button" value="打印版本號" onclick="run1()" />
<script async type="text/javascript" src="ffmpeg_decoder.js"></script>
</body>
</html>
cmd命令行運行python,開啟http服務器。
python -m http.server
打開谷歌瀏覽器,輸入http://127.0.0.1:8000/index.html地址,按下F12打開控制臺,點擊頁面上的按鈕看控制臺輸出。
完成調用,已成功打印版本號。
wasm編譯的ffmpeg代碼,不能使用avformat_open_input 直接打開文件地址,打開網絡地址,只能從內存中讀取數據進行解碼。前端js加載了本地磁盤文件后,需要通過內存方式傳遞給wasm-ffmpeg接口里,然后ffmpeg再進行解碼。
下面C語言代碼里演示了調用ffmpeg解碼內存里視頻文件過程,解碼讀取分辨率、總時間,解幀數據等。代碼只是為了演示如何調用ffmpeg的測試代碼,代碼比較簡單,只是解碼了第一幀數據,得到了YUV420P數據,然后保存在文件中。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/imgutils.h>
#include <libswresample/swresample.h>
#include <emscripten/emscripten.h>
#include <libavcodec/version.h>
//EMSCRIPTEN_KEEPALIVE
/*
存儲視頻文件到磁盤
參數:
char *name 文件名稱
char *buf 寫入的數據
unsigned int len 寫入長度
*/
int write_file(char *name, char *buf, unsigned int len)
{
//創建文件
FILE *new_fp = fopen(name, "wb");
if (new_fp == NULL)
{
printf("%s 文件創建失敗.\n", name);
return -1;
}
else
{
printf("%s 文件創建成功.\n", name);
}
//寫入磁盤
int cnt = fwrite(buf, 1, len, new_fp);
printf("成功寫入=%d 字節\n", cnt);
//關閉文件
fclose(new_fp);
return cnt;
}
/*
獲取文件大小
*/
long get_FileSize(char *name)
{
/*1. 打開文件*/
FILE *fp = fopen(name, "rb");
if (fp == NULL)
{
printf("% 文件不存在.\n", name);
return -1;
}
/*2. 將文件指針偏移到文件結尾*/
fseek(fp, 0, SEEK_END);
/*3. 獲取當前文件指針距離文件頭的字節偏移量*/
long byte = ftell(fp);
/*4. 關閉文件*/
fclose(fp);
return byte;
}
/*
讀文件
char *buf
*/
unsigned char *read_file(char *name)
{
//創建文件
FILE *fp = fopen(name, "rb");
if (fp == NULL)
{
printf("%s 文件打開失敗.\n", name);
return -1;
}
//獲取文件大小
int size = get_FileSize(name);
//申請空間
unsigned char *buf = (unsigned char *)malloc(size);
if (buf == NULL)
{
printf("空間申請失敗:%d byte.\n", size);
return NULL;
}
//讀取文件到內存
int cnt = fread(buf, 1, size, fp);
printf("成功讀取=%d 字節\n", cnt);
//關閉文件
fclose(fp);
return buf;
}
//獲取版本號
void print_version()
{
unsigned codecVer = avcodec_version();
int ver_major, ver_minor, ver_micro;
ver_major = (codecVer >> 16) & 0xff;
ver_minor = (codecVer >> 8) & 0xff;
ver_micro = (codecVer) & 0xff;
printf("當前ffmpeg的版本:avcodec version is: %d=%d.%d.%d\n", codecVer, ver_major, ver_minor, ver_micro);
}
int ffmpeg_laliu_run_flag = 1;
/*
功能: 這是FFMPEG回調函數,返回1表示超時 0表示正常
ffmpeg阻塞完成一些任務的時候,可以快速強制退出.
*/
static int interrupt_cb(void *ctx)
{
if (ffmpeg_laliu_run_flag == 0)return 1;
return 0;
}
//存放視頻解碼的詳細信息
struct M_VideoInfo
{
int64_t duration;
int video_width;
int video_height;
};
struct M_VideoInfo m_VideoInfo;
//讀取數據的回調函數-------------------------
//AVIOContext使用的回調函數!
//注意:返回值是讀取的字節數
//手動初始化AVIOContext只需要兩個東西:內容來源的buffer,和讀取這個Buffer到FFmpeg中的函數
//回調函數,功能就是:把buf_size字節數據送入buf即可
//第一個參數(void *opaque)一般情況下可以不用
/*正確方式*/
struct buffer_data
{
uint8_t *ptr; /* 文件中對應位置指針 */
size_t size; ///< size left in the buffer /* 文件當前指針到末尾 */
};
// 重點,自定的buffer數據要在外面這里定義
struct buffer_data bd = { 0 };
//用來將內存buffer的數據拷貝到buf
int read_packet(void *opaque, uint8_t *buf, int buf_size)
{
buf_size = FFMIN(buf_size, bd.size);
if (!buf_size)
return AVERROR_EOF;
printf("ptr:%p size:%zu bz%zu\n", bd.ptr, bd.size, buf_size);
/* copy internal buffer data to buf */
memcpy(buf, bd.ptr, buf_size);
bd.ptr += buf_size;
bd.size -= buf_size;
return buf_size;
}
//ffmpeg解碼使用的全局變量
unsigned char * iobuffer;
AVFormatContext * format_ctx;
int video_width = 0;
int video_height = 0;
int video_stream_index = -1;
char* video_buffer;
/*
函數功能: 初始化解碼環境
函數參數:
unsigned char *buf 視頻文件的內存地址
unsigned int len 視頻文件長度
*/
int initDecoder(unsigned char *buf,unsigned int len)
{
int ret = 0;
bd.ptr = buf; /* will be grown as needed by the realloc above */
bd.size = len; /* no data at this point */
//注冊ffmpeg
av_register_all();
unsigned int version = avformat_version();
printf("ffmpeg版本: %d\r\n",version);
// Allocate an AVFormatContext
format_ctx = avformat_alloc_context();
if (format_ctx == NULL)
{
printf("avformat_alloc_context 失敗.\n");
return -1;
}
iobuffer = (unsigned char *)av_malloc(32768);
AVIOContext *avio = avio_alloc_context(iobuffer, 32768, 0, NULL, read_packet, NULL, NULL);
format_ctx->pb = avio;
ret = avformat_open_input(&format_ctx, "nothing", NULL, NULL);
format_ctx->interrupt_callback.callback = interrupt_cb; //--------注冊回調函數
AVDictionary* options = NULL;
//ret = avformat_open_input(&format_ctx, url, NULL, NULL);
if (ret != 0)
{
char buf[1024];
av_strerror(ret, buf, 1024);
printf("無法打開視頻內存,return value: %d \n",ret);
return -1;
}
printf("正在讀取媒體文件的數據包以獲取流信息.\n");
// 讀取媒體文件的數據包以獲取流信息
ret = avformat_find_stream_info(format_ctx, NULL);
if (ret < 0)
{
printf("無法獲取流信息: %d\n",ret);
return -1;
}
AVCodec *video_pCodec;
// audio/video stream index
printf("視頻中流的數量: %d\n",format_ctx->nb_streams);
printf("視頻總時間:%lld 秒\n",format_ctx->duration / AV_TIME_BASE);
//得到秒單位的總時間
m_VideoInfo.duration = format_ctx->duration / AV_TIME_BASE;
for (int i = 0; i < format_ctx->nb_streams; ++i)
{
const AVStream* stream = format_ctx->streams[i];
printf("編碼數據的類型: %d\n",stream->codecpar->codec_id);
if (stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
{
//查找解碼器
video_pCodec = avcodec_find_decoder(AV_CODEC_ID_H264);
//打開解碼器
int err = avcodec_open2(stream->codec, video_pCodec, NULL);
if (err != 0)
{
printf("H264解碼器打開失敗.\n");
return 0;
}
video_stream_index = i;
//得到視頻幀的寬高
video_width = stream->codecpar->width;
video_height = stream->codecpar->height;
//保存寬和高
m_VideoInfo.video_height = video_height;
m_VideoInfo.video_width = video_width;
//解碼后的YUV數據存放空間
video_buffer = malloc(video_height * video_width * 3 / 2);
printf("視頻幀的尺寸(以像素為單位): (寬X高)%dx%d 像素格式: %d\n",
stream->codecpar->width,stream->codecpar->height,stream->codecpar->format);
}
else if (stream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO)
{
}
}
if (video_stream_index == -1)
{
printf("沒有檢測到視頻流.\n");
return -1;
}
printf("初始化成功.\n");
return 0;
}
//獲取視頻總時長
int64_t GetVideoDuration()
{
return m_VideoInfo.duration;
}
//獲取視頻寬
int64_t GetVideoWidth()
{
return m_VideoInfo.video_width;
}
//獲取視頻高
int64_t GetVideoHeight()
{
return m_VideoInfo.video_height;
}
//獲取視頻幀
//傳入參數時間單位--秒
unsigned char *GetVideoFrame(int time)
{
AVPacket pkt;
double video_clock;
AVFrame *SRC_VIDEO_pFrame = av_frame_alloc();
printf("開始解碼.\n");
printf("跳轉狀態:%d\n",av_seek_frame(format_ctx, -1, time*AV_TIME_BASE, AVSEEK_FLAG_ANY));
while (1)
{
int var = av_read_frame(format_ctx, &pkt);
//讀取一幀數據
if (var < 0)
{
printf("數據讀取完畢:%d\n", var);
break;
}
printf("開始..\n");
//如果是視頻流節點
if (pkt.stream_index == video_stream_index)
{
//當前時間
video_clock = av_q2d(format_ctx->streams[video_stream_index]->time_base) * pkt.pts;
printf("pkt.pts=%0.2f,video_clock=%0.2f\n", pkt.pts, video_clock);
//解碼視頻 frame
//發送視頻幀
if (avcodec_send_packet(format_ctx->streams[video_stream_index]->codec, &pkt) != 0)
{
av_packet_unref(&pkt);//不成功就釋放這個pkt
continue;
}
//接受后對視頻幀進行解碼
if (avcodec_receive_frame(format_ctx->streams[video_stream_index]->codec, SRC_VIDEO_pFrame) != 0)
{
av_packet_unref(&pkt);//不成功就釋放這個pkt
continue;
}
//轉格式
/* sws_scale(img_convert_ctx,
(uint8_t const **)SRC_VIDEO_pFrame->data,
SRC_VIDEO_pFrame->linesize, 0,video_height, RGB24_pFrame->data,
RGB24_pFrame->linesize);*/
memset(video_buffer, 0, video_height * video_width * 3 / 2);
int height = video_height;
int width = video_width;
printf("decode video ok\n");
int a = 0, i;
for (i = 0; i < height; i++)
{
memcpy(video_buffer + a, SRC_VIDEO_pFrame->data[0] + i * SRC_VIDEO_pFrame->linesize[0], width);
a += width;
}
for (i = 0; i < height / 2; i++)
{
memcpy(video_buffer + a, SRC_VIDEO_pFrame->data[1] + i * SRC_VIDEO_pFrame->linesize[1], width / 2);
a += width / 2;
}
for (i = 0; i < height / 2; i++)
{
memcpy(video_buffer + a, SRC_VIDEO_pFrame->data[2] + i * SRC_VIDEO_pFrame->linesize[2], width / 2);
a += width / 2;
}
//保存在文件中:
//write_file("./666.yuv", video_buffer, video_height * video_width * 3 / 2);
printf("退出成功....\n");
break;
}
//釋放包
av_packet_unref(&pkt);
}
av_free(SRC_VIDEO_pFrame);
return video_buffer;
}
//銷毀內存
void DeleteMemory()
{
//釋放空間
av_free(iobuffer);
}
emcc wasm_ffmpeg/wasm_ffmpeg.c ffmpeg-4.4-wasm/lib/libavformat.a ffmpeg-4.4-wasm/lib/libavcodec.a ffmpeg-4.4-wasm/lib/libswresample.a ffmpeg-4.4-wasm/lib/libavutil.a -I "ffmpeg-4.4-wasm/include" -s EXPORTED_FUNCTIONS="['_malloc','_free','ccall','allocate','UTF8ToString','_initDecoder','_write_file','_print_version','_get_FileSize','_read_file','_GetVideoFrame','_GetVideoWidth','_GetVideoDuration','_GetVideoHeight','_DeleteMemory']" -s WASM=1 -s ASSERTIONS=0 -s TOTAL_MEMORY=167772160 -s ALLOW_MEMORY_GROWTH=1 -o out/ffmpeg_decoder.js
編譯成功后生成的wasm和js文件:
完成了視頻選擇,播放,調用了C語言編寫的接口完成解碼返回,但是沒有渲染。
<!doctype html>
<html lang="en-us">
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>js調用c語言函數示例</title>
</head>
<body>
<input id="myfile" type="file"/>
<video id="output-video" width="300" controls></video>
<div><canvas id="glcanvas" width="640" height="480"></canvas></div>
<script>
//代碼摘自:https://github.com/ivan-94/video-push/blob/master/yuv/index.html#L312
const video = document.getElementById('glcanvas');
let renderer;
class WebglScreen {
constructor(canvas) {
this.canvas = canvas;
this.gl =
canvas.getContext('webgl') ||
canvas.getContext('experimental-webgl');
this._init();
}
_init() {
let gl = this.gl;
if (!gl) {
console.log('gl not support!');
return;
}
// 圖像預處理
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
// GLSL 格式的頂點著色器代碼
let vertexShaderSource = `
attribute lowp vec4 a_vertexPosition;
attribute vec2 a_texturePosition;
varying vec2 v_texCoord;
void main() {
gl_Position = a_vertexPosition;
v_texCoord = a_texturePosition;
}
`;
let fragmentShaderSource = `
precision lowp float;
uniform sampler2D samplerY;
uniform sampler2D samplerU;
uniform sampler2D samplerV;
varying vec2 v_texCoord;
void main() {
float r,g,b,y,u,v,fYmul;
y = texture2D(samplerY, v_texCoord).r;
u = texture2D(samplerU, v_texCoord).r;
v = texture2D(samplerV, v_texCoord).r;
fYmul = y * 1.1643828125;
r = fYmul + 1.59602734375 * v - 0.870787598;
g = fYmul - 0.39176171875 * u - 0.81296875 * v + 0.52959375;
b = fYmul + 2.01723046875 * u - 1.081389160375;
gl_FragColor = vec4(r, g, b, 1.0);
}
`;
let vertexShader = this._compileShader(
vertexShaderSource,
gl.VERTEX_SHADER,
);
let fragmentShader = this._compileShader(
fragmentShaderSource,
gl.FRAGMENT_SHADER,
);
let program = this._createProgram(vertexShader, fragmentShader);
this._initVertexBuffers(program);
// 激活指定的紋理單元
gl.activeTexture(gl.TEXTURE0);
gl.y = this._createTexture();
gl.uniform1i(gl.getUniformLocation(program, 'samplerY'), 0);
gl.activeTexture(gl.TEXTURE1);
gl.u = this._createTexture();
gl.uniform1i(gl.getUniformLocation(program, 'samplerU'), 1);
gl.activeTexture(gl.TEXTURE2);
gl.v = this._createTexture();
gl.uniform1i(gl.getUniformLocation(program, 'samplerV'), 2);
}
/**
* 初始化頂點 buffer
* @param {glProgram} program 程序
*/
_initVertexBuffers(program)
{
let gl = this.gl;
let vertexBuffer = gl.createBuffer();
let vertexRectangle = new Float32Array([
1.0,
1.0,
0.0,
-1.0,
1.0,
0.0,
1.0,
-1.0,
0.0,
-1.0,
-1.0,
0.0,
]);
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
// 向緩沖區寫入數據
gl.bufferData(gl.ARRAY_BUFFER, vertexRectangle, gl.STATIC_DRAW);
// 找到頂點的位置
let vertexPositionAttribute = gl.getAttribLocation(
program,
'a_vertexPosition',
);
// 告訴顯卡從當前綁定的緩沖區中讀取頂點數據
gl.vertexAttribPointer(
vertexPositionAttribute,
3,
gl.FLOAT,
false,
0,
0,
);
// 連接vertexPosition 變量與分配給它的緩沖區對象
gl.enableVertexAttribArray(vertexPositionAttribute);
let textureRectangle = new Float32Array([
1.0,
0.0,
0.0,
0.0,
1.0,
1.0,
0.0,
1.0,
]);
let textureBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, textureBuffer);
gl.bufferData(gl.ARRAY_BUFFER, textureRectangle, gl.STATIC_DRAW);
let textureCoord = gl.getAttribLocation(program, 'a_texturePosition');
gl.vertexAttribPointer(textureCoord, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(textureCoord);
}
/**
* 創建并編譯一個著色器
* @param {string} shaderSource GLSL 格式的著色器代碼
* @param {number} shaderType 著色器類型, VERTEX_SHADER 或 FRAGMENT_SHADER。
* @return {glShader} 著色器。
*/
_compileShader(shaderSource, shaderType)
{
// 創建著色器程序
let shader = this.gl.createShader(shaderType);
// 設置著色器的源碼
this.gl.shaderSource(shader, shaderSource);
// 編譯著色器
this.gl.compileShader(shader);
const success = this.gl.getShaderParameter(
shader,
this.gl.COMPILE_STATUS,
);
if (!success) {
let err = this.gl.getShaderInfoLog(shader);
this.gl.deleteShader(shader);
console.error('could not compile shader', err);
return;
}
return shader;
}
/**
* 從 2 個著色器中創建一個程序
* @param {glShader} vertexShader 頂點著色器。
* @param {glShader} fragmentShader 片斷著色器。
* @return {glProgram} 程序
*/
_createProgram(vertexShader, fragmentShader)
{
const gl = this.gl;
let program = gl.createProgram();
// 附上著色器
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
// 將 WebGLProgram 對象添加到當前的渲染狀態中
gl.useProgram(program);
const success = this.gl.getProgramParameter(
program,
this.gl.LINK_STATUS,
);
if (!success) {
console.err(
'program fail to link' + this.gl.getShaderInfoLog(program),
);
return;
}
return program;
}
/**
* 設置紋理
*/
_createTexture(filter = this.gl.LINEAR)
{
let gl = this.gl;
let t = gl.createTexture();
// 將給定的 glTexture 綁定到目標(綁定點
gl.bindTexture(gl.TEXTURE_2D, t);
// 紋理包裝 參考https://github.com/fem-d/webGL/blob/master/blog/WebGL基礎學習篇(Lesson%207).md -> Texture wrapping
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
// 設置紋理過濾方式
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filter);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter);
return t;
}
/**
* 渲染圖片出來
* @param {number} width 寬度
* @param {number} height 高度
*/
renderImg(width, height, data)
{
let gl = this.gl;
// 設置視口,即指定從標準設備到窗口坐標的x、y仿射變換
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
// 設置清空顏色緩沖時的顏色值
gl.clearColor(0, 0, 0, 0);
// 清空緩沖
gl.clear(gl.COLOR_BUFFER_BIT);
let uOffset = width * height;
let vOffset = (width >> 1) * (height >> 1);
gl.bindTexture(gl.TEXTURE_2D, gl.y);
// 填充紋理
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.LUMINANCE,
width,
height,
0,
gl.LUMINANCE,
gl.UNSIGNED_BYTE,
data.subarray(0, uOffset),
);
gl.bindTexture(gl.TEXTURE_2D, gl.u);
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.LUMINANCE,
width >> 1,
height >> 1,
0,
gl.LUMINANCE,
gl.UNSIGNED_BYTE,
data.subarray(uOffset, uOffset + vOffset),
);
gl.bindTexture(gl.TEXTURE_2D, gl.v);
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.LUMINANCE,
width >> 1,
height >> 1,
0,
gl.LUMINANCE,
gl.UNSIGNED_BYTE,
data.subarray(uOffset + vOffset, data.length),
);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
}
/**
* 根據重新設置 canvas 大小
* @param {number} width 寬度
* @param {number} height 高度
* @param {number} maxWidth 最大寬度
*/
setSize(width, height, maxWidth)
{
let canvasWidth = Math.min(maxWidth, width);
this.canvas.width = canvasWidth;
this.canvas.height = (canvasWidth * height) / width;
}
destroy()
{
const { gl } = this;
gl.clear(
gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT,
);
}
} // end of webgl
const initialCanvas = (canvas, width, height) => {
canvas.width = width;
canvas.height = height;
return new WebglScreen(canvas);
};
const render = (buff,width,height) =>
{
if (renderer == null) {
return;
}
renderer.renderImg(width, height, buff);
};
</script>
<script type='text/javascript'>
function run1()
{
}
function run2()
{
}
//加載本地文件
var file=document.getElementById("myfile");
file.onchange=function(event){
let fileReader = new FileReader();
fileReader.onload = function(){
// 當 FileReader 讀取文件時候,讀取的結果會放在 FileReader.result 屬性中
var fileArray= this.result;
console.log(fileArray);
let fileBuffer = new Uint8Array(this.result);
console.log(fileBuffer);
//申請空間
var fileBufferPtr = _malloc(fileBuffer.length)
//將fileBuffer里的內容拷貝到fileBufferPtr里
Module.HEAP8.set(fileBuffer,fileBufferPtr)
//1. 寫文件
//申請空間,存放字符串
//var name = allocate(intArrayFromString("./tmp.mp4"), ALLOC_NORMAL);
//var run_var=_write_file(name,fileBufferPtr,fileBuffer.length);
//console.log('寫文件成功字節數:',run_var);
//2. 獲取文件大小
//var file_size=_get_FileSize(name);
//console.log('獲取文件大小:',file_size);
//const data = ffmpeg.FS('readFile', 'output.mp4');
//3. 讀取文文件
//const data = _read_file(name);
// const video = document.getElementById('output-video');
//video.src = URL.createObjectURL(new Blob([fileBuffer.buffer], { type: 'video/mp4' }));
//加載內存數據
// Module.HEAPU8.subarray(imgBufferPtr, data);
//4. 初始化解碼器,加載文件
_initDecoder(fileBufferPtr,fileBuffer.length);
//5. 獲取總時間
var time=_GetVideoDuration();
console.log('視頻總時間:'+time);
//6. 獲取視頻寬
var Width=_GetVideoWidth();
console.log('視頻寬:'+Width);
//7. 獲取視頻高
var Height=_GetVideoHeight();
console.log('視頻高:'+Height);
renderer = initialCanvas(video,Width,Height);
//申請空間,存放字符串
//var name_file = allocate(intArrayFromString("./666.yuv"), ALLOC_NORMAL);
//讀取文件
//var yuv_wasm_data=_read_file(name_file);
//8. 獲取視頻幀
var yuv_wasm_data=_GetVideoFrame(10);
var renderlength=Width*Height*3/2;
var RenderBuffer = new Uint8Array (Module.HEAPU8.subarray(yuv_wasm_data,yuv_wasm_data + renderlength + 1) );
console.log(RenderBuffer);
render(RenderBuffer,Width,Height);
};
fileReader.readAsArrayBuffer(this.files[0]);
}
</script>
<input type="button" value="載入文件初始化解碼器" onclick="run1()" />
<script async type="text/javascript" src="ffmpeg_decoder.js"></script>
</body>
</html>
命令行運行命令,開啟HTTP服務器,方便測試:
python -m http.server
打開谷歌瀏覽器,輸入http://127.0.0.1:8000/index.html地址,按下F12打開控制臺,點擊頁面上的按鈕看控制臺輸出。
(1)輸入地址,打開網頁
(2)按下F12,打開控制臺
(3)選擇一個MP4文件載入測試。獲取一幀圖片。
隨著物聯網技術不斷發展,視頻監控系統在各個領域的應用越來越廣泛。其中,RTSP(Real Time Streaming Protocol)是一種常用的流媒體傳輸協議,可以實現對實時音視頻數據的傳輸和播放。為了實現視頻監控系統的網絡化和智能化,需要開發一個基于RTSP協議的視頻流服務器,能夠接收前端設備的視頻流,并提供RTSP協議的服務,方便客戶端進行實時的視頻瀏覽、回放等操作。
在開發過程中,為了提高開發效率、減少開發難度和成本,同時具備良好的可擴展性和可維護性,我選擇使用Qt和Live555庫來搭建RTSP服務器。Qt是一個跨平臺的C++應用程序開發框架,具有完善的GUI界面設計工具和豐富的功能模塊,可以大大簡化開發過程;而Live555是一個跨平臺的流媒體開發庫,支持多種流媒體協議,包括RTSP、SIP、RTP等,可以幫助我們快速實現視頻流的傳輸和處理。
該項目將主要實現以下功能:
RTSP服務器是一種提供流媒體服務的服務器,它采用RTSP協議與客戶端進行通信,支持音視頻數據的傳輸和控制。RTSP(Real-Time Streaming Protocol)實時流傳輸協議是一個應用層協議,通過TCP或UDP傳輸數據,用于實現多媒體數據的實時傳輸。
RTSP服務器主要用于流媒體直播、點播、錄像等應用場景,可以讓用戶通過網絡實時觀看視頻、聽取音頻等。RTSP服務器一般具有以下功能:
常見的RTSP服務器軟件包括Live555、Wowza Media Server、Darwin Streaming Server等。使用RTSP服務器可以輕松實現基于網絡的流媒體服務,滿足直播、視頻會議、遠程監控等應用場景需求。
Live555庫是一個開源的多媒體流媒體服務框架,它提供了一系列的C++類和庫函數,用于開發基于標準網絡協議的流媒體應用程序。該庫主要用于實現RTP/RTCP、RTSP、SIP以及SDP等標準協議,可以方便地實現視頻/音頻的網絡傳輸、播放、錄制和轉碼等功能。
Live555庫具有以下特點:
使用Live555庫進行開發,可以快速搭建基于網絡的流媒體應用程序。例如,可以使用該庫實現RTSP服務器或客戶端,實現視頻直播、遠程監控、視頻會議等應用場景。同時,Live555庫也可以作為其他流媒體服務器軟件的基礎組件,例如: Wowza Media Server、Darwin Streaming Server等。
在Qt中搭建RTSP服務器,可以使用開源的Live555庫。Live555是一個跨平臺、C++語言編寫的多媒體開發庫,支持RTSP和SIP等常見協議,并且有完整的服務器和客戶端實現。
下面是在Qt中基于Live555庫實現簡單的RTSP服務器的步驟:
【1】下載并安裝Live555庫。從官網(http://www.live555.com/liveMedia/#download)下載最新版本的庫。
并按照文檔說明進行安裝。將Live555庫的路徑添加到Qt Creator的項目配置文件中。
【2】創建一個Qt控制臺應用程序。在Qt Creator中創建一個空的控制臺應用程序,并在項目的.pro文件中添加Live555庫的鏈接選項,例如:
LIBS += -LLive555庫的路徑 -lliveMedia -lgroupsock -lUsageEnvironment -lBasicUsageEnvironment
【3】編寫RTSP服務器代碼。創建一個類繼承自live555庫中的RTSPServer類,并實現相應的虛函數,createNewSession()和deleteStream()。
【4】啟動RTSP服務器。在main()函數中創建RTSP服務器對象,并調用start()函數啟動服務器,如下所示:
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
RTSPServer* server = new MyRTSPServer();
server->start();
return a.exec();
}
【5】測試。使用RTSP客戶端工具(如VLC播放器)連接本地的RTSP服務器,并播放視頻流。
下面是使用Qt+Live555搭建RTSP服務器的核心代碼示例:
#include <liveMedia.hh>
#include <BasicUsageEnvironment.hh>
class VideoStreamSource : public FramedSource {
public:
static VideoStreamSource* createNew(UsageEnvironment& env) {
return new VideoStreamSource(env);
}
virtual void doGetNextFrame() {
// 將視頻數據幀復制到fTo處并設置fFrameSize和fNumTruncatedBytes,然后調用afterGetting()函數通知視頻幀可用。
if (condition1 && condition2) {
memcpy(fTo, fVideoFrame, fVideoFrameSize);
afterGetting(this);
} else {
handleClosure(this);
}
}
protected:
VideoStreamSource(UsageEnvironment& env) : FramedSource(env) {
// 初始化一些變量
}
virtual ~VideoStreamSource() {}
private:
// 一些成員變量
char* fVideoFrame;
unsigned fVideoFrameSize;
};
class MyRTSPServer : public RTSPServer {
public:
static MyRTSPServer* createNew(UsageEnvironment& env, Port ourPort) {
return new MyRTSPServer(env, ourPort);
}
protected:
MyRTSPServer(UsageEnvironment& env, Port ourPort)
: RTSPServer(env, ourPort, NULL) {}
virtual ~MyRTSPServer() {}
virtual ServerMediaSession* lookupSession(char const* streamName, Boolean isFirstLookup) {
ServerMediaSession* session = RTSPServer::lookupSession(streamName, isFirstLookup);
if (session == NULL) {
// 創建一個新的會話以支持RTSP客戶端請求的視頻流
session = ServerMediaSession::createNew(envir(), streamName);
// 將視頻幀添加到會話中
VideoStreamSource* videoSource = VideoStreamSource::createNew(envir());
session->addSubsession(MPEG4VideoStreamDiscreteFramer::createNew(envir(), videoSource, false));
addServerMediaSession(session);
}
return session;
}
};
int main(int argc, char *argv[]) {
// 創建一個QT應用程序實例
QCoreApplication app(argc, argv);
// 創建一個RTSP服務器實例,并監聽9090端口
MyRTSPServer* rtspServer = MyRTSPServer::createNew(*(app.instance()), 9090);
if (rtspServer == NULL) {
qDebug() << "Failed to create RTSP server: " << env.getResultMsg() << endl;
exit(1);
}
// 啟動Qt事件循環
return app.exec();
}
上面的代碼實現了以下幾個功能: