aardio 文档

sys.mediaFoundation

调用 Windows 自带的 Media Foundation 读写音视频,不需要下载任何组件。

写视频

下面生成一段 3 秒、带声音的 720p 视频。每写一帧画面,就写入这一帧时长的音频。

import sys.mediaFoundation.writer;
import gdip.graphics;
import gdip.solidBrush;

var W,H,FPS,SR = 1280,720,30,48000;
var writer,err = sys.mediaFoundation.writer("/demo.mp4",{
    width = W; height = H; fps = FPS;
    audio = {sampleRate = SR; channels = 2};
});
if(!writer) return print(err);

var bmp = gdip.bitmap(W,H,0xE200B/*PARGB*/);
var g = gdip.graphics(bmp);
var brush = gdip.solidBrush(0xFFE0A030);
var spf = SR / FPS; //每帧对应的采样数

for(i=0;FPS*3-1){
    g.clear(0xFF202020);
    g.fillEllipse(brush,100 + i*10,300,120,120);
    writer.writeFrame(bmp);

    //这一帧时长的音频:440Hz 正弦,左右声道交错
    var samples = {};
    for(k=0;spf-1){
        var v = math.floor(math.sin(2*math.pi*440*(i*spf+k)/SR) * 8000);
        samples[k*2+1] = v;
        samples[k*2+2] = v;
    }
    writer.writeAudio(raw.buffer({word s[] = samples}));
}

brush.delete();
g.delete();
bmp.dispose();
writer.close(); //必须调用,否则 MP4 文件不完整

写入器选项

字段 说明
width,height 视频尺寸,必须是偶数。省略则只写音频(可生成 M4A)
fps 帧率,默认 30。29.97、23.976、59.94 自动转为 N*1000/1001,也可以直接写 {30000,1001}
quality 画质 1~100,默认 80。按固定 QP 编码,文件大小随画面复杂度变化,见下文
videoBitrate 改为按平均码率编码(bps,VBR),需要控制文件大小时使用。与 quality 只能指定一个
profile "high"(默认)、"main"、"baseline"
gop 关键帧间隔(帧数),默认 2 秒
hardware 是否允许硬件编码器,默认 false(软件编码),见下文
encoder 编码器参数表,键为 CODECAPI_ 名称(可省略前缀),最后写入,可覆盖以上设置
audio {sampleRate=48000; channels=2; bitrate=192000},省略则不写音频

AAC 只支持 44100 或 48000 采样率,1、2、6 声道;码率只能是 96/128/160/192 kbps,其他值会取最接近的一档。

画质与文件大小

默认 quality=80:关键帧(I 帧)QP 12,其他帧(P 帧)QP 20。writer.rateControl 记录实际使用的设置,例如 {mode="quality";quality=80;qpI=12;qpP=20}。

import sys.mediaFoundation.writer;
var writer = sys.mediaFoundation.writer("/demo-quality.mp4",{width = 1920; height = 1080; fps = 30; quality = 90});
print(writer.rateControl.qpI,writer.rateControl.qpP); //12 16
writer.writeFrame(raw.buffer(1920*1080*4),30); //1 秒黑屏:同一帧重复 30 次
writer.close();

下表是 1080p30 测试片段(12~20px 小字、1px 细线、暗部渐变、照片、滚动字幕、粒子、硬切换)的实测结果,PSNR 越高越接近原画:

设置 码率 小字 PSNR 细线 PSNR 问题
按码率 CBR 12 Mbps(软件) 7.3 Mbps 45.5 dB,首帧 38.4 43.4 dB,首帧 35.8 开头一秒明显发虚
编码器自带质量模式(关键帧与其他帧同 QP) 5.2 Mbps 45.6 dB 43.8 dB,关键帧 35.4 每 2 秒闪一下
quality=80(默认,软件编码) 5.0 Mbps 48.2 dB 46.7 dB —
quality=80 + hardware=true(Intel 核显 QSV) 4.5 Mbps 49.0 dB 48.4 dB 结果因显卡厂商和驱动而异

软件编码与硬件编码

默认使用系统自带的软件编码器(hardware=false),原因:

hardware=true 时使用同样的画质设置。在测试机的 Intel 核显上,同样的 quality 硬件编码文件略小、画质略好(见上表),但换一台电脑结果可能完全不同。其他厂商的编码器如果不支持固定 QP,创建写入器会返回错误,这时可改用 videoBitrate。照片、实拍素材较多时可以试试 hardware=true,并用读取器回读检查画质。

帧数据

writer.writeFrame(frame,repeatCount) 的 frame 可以是:

repeatCount 表示把同一帧连续写入多次(共用同一份数据),适合静止画面。时间戳按帧号自动计算,长时间写入也不会漂移。

音频数据

writer.writeAudio(pcm) 写入 int16 交错 PCM(立体声为 L,R,L,R…),字节数必须是 声道数×2 的整数倍。可以分块多次写入,时间戳按已写入的采样数自动计算。

音视频要交替写入:如果先写完全部画面再写声音,系统会在内存里缓存整段视频,长视频会耗尽 32 位进程的内存。writer.videoTime 与 writer.audioTime 记录了两路已写入的时长,可以据此对齐。

读取与解码

import sys.mediaFoundation.reader;

var reader,err = sys.mediaFoundation.reader("/demo.mp4");
if(!reader) return print(err);

var info = reader.info;
print(info.duration,info.video[["width"]],info.video[["height"]],info.audio[["sampleRate"]]);

for(bitmap,time,frame in reader.eachFrame()){
    //bitmap 是读取器复用的 PARGB 位图,需要保留时调用 bitmap.copy()
    if(frame == 0) bitmap.save("/first.png");
}

var pcm,startTime = reader.readAudio(); //int16 交错 PCM(buffer)
reader.close();

视频和音频各用一个系统读取器,读取位置互不影响,只读其中一路时也不会在内存里堆积另一路的数据。

颜色、时间与性能

以下行为已在 Windows 11 上用库自带的验收测试实测确认,并用 Chromium(WebView2)独立解码核对过。

注意事项

底层接口

sys.mediaFoundation 提供了 IMFAttributes、IMFMediaType、IMFSample、IMFMediaBuffer、IMF2DBuffer、IMFSinkWriter、IMFSourceReader 接口声明(方法顺序按 Windows SDK 头文件)、常用 GUID 表 guids,以及按名称批量设置属性的函数:

import sys.mediaFoundation;
var mf = sys.mediaFoundation;
mf.startup();

var mediaType = mf.createMediaType({
    MF_MT_MAJOR_TYPE = "MFMediaType_Video"; //字符串写入 GUID
    MF_MT_SUBTYPE = "MFVideoFormat_H264";
    MF_MT_AVG_BITRATE = 8000000;            //数值写入 UINT32
    MF_MT_FRAME_SIZE = {1920,1080};         //{高32位,低32位} 写入 UINT64
});
var width,height = mf.getPair(mediaType,"MF_MT_FRAME_SIZE");
print(width,height,mf.guidName(mf.getGUID(mediaType,"MF_MT_SUBTYPE")));

com.Release(mediaType);
mf.shutdown();

writer.sinkWriter 是写入器内部的 IMFSinkWriter 接口对象,可用于库没有封装的高级操作。

Markdown 格式