# sys.mediaFoundation

调用 Windows 自带的 Media Foundation 读写音视频，不需要下载任何组件。

- `sys.mediaFoundation.writer`：写 MP4（H.264 视频 + AAC 音频），帧来自 gdip 位图，声音来自 int16 PCM。
- `sys.mediaFoundation.reader`：读取并解码 MP4、MOV、M4A、MP3、WAV、WMV 等文件，逐帧得到 gdip 位图，音频得到 int16 PCM。
- `sys.mediaFoundation`：底层接口声明、GUID 表和属性工具函数，供高级用法使用。
- `sys.mediaFoundation.mp3Decoder`：把内存中陆续到达的 MP3 字节增量解码为 int16 PCM，详见 [MP3 增量解码](mp3Decoder/index.html)。
- `sys.mediaFoundation.streamingMp3Player`：流式 MP3 播放器，边收边播，提供 `write/finish/close`，详见 [流式 MP3 播放](streamingMp3Player/index.html)。它由 `mp3Decoder` 解码、[sys.waveOut](https://www.aardio.com/zh-cn/docs/library/sys/waveOut/) 输出 PCM。

## 写视频

下面生成一段 3 秒、带声音的 720p 视频。每写一帧画面，就写入这一帧时长的音频。

```aardio
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}`。

- 换算规则：P 帧 QP = round(52 − 0.4 × quality)，范围 12~51；关键帧 QP = P 帧 QP − 8，并限制在 12~14。例如 60 → 14/28，70 → 14/24，90 → 12/16。
- 文件大小随画面复杂度变化：静止画面几乎不占码率，全屏运动的照片最占码率。1080p30 实测，动态图形约 5 Mbps，全屏照片缓慢推近约 8 Mbps；quality=70 约 3.5 Mbps，quality=90 约 7 Mbps。
- 关键帧 QP 必须足够低，这是刻意的。关键帧 QP 高于 14（包括与其他帧相同）时，静止的小字和细线每到关键帧（默认每 2 秒）就会变虚一帧，看起来像周期性闪烁。
- 静止内容在后续帧里直接沿用关键帧的画面，所以调低 quality 主要降低运动内容的画质，静止文字始终清晰。需要限制文件大小时也可以改用 `videoBitrate`。

```aardio
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`），原因：

- **结果一致**：同一脚本在任何 Windows 电脑上输出相同的画质和文件大小，自动验收的结论可以复用。硬件编码器因厂商和驱动而异，支持的参数和码率控制行为都不同，例如 Intel QSV 在 CBR 模式下会把文件填满到目标码率。
- **帧本来就在内存里**：gdip 在 CPU 内存里绘制，软件编码器直接读取；硬件编码要先把每帧送进显卡的编码缓冲区，编码后再取回码流。
- **编码不是瓶颈**：编码在系统工作线程里与绘制并行，总耗时主要取决于绘制。
- **硬件编码的长处用不上**：省 CPU、实时编码对离线生成视频没有意义。生成视频不必在意 CPU 占用，画质才是重点。

`hardware=true` 时使用同样的画质设置。在测试机的 Intel 核显上，同样的 quality 硬件编码文件略小、画质略好（见上表），但换一台电脑结果可能完全不同。其他厂商的编码器如果不支持固定 QP，创建写入器会返回错误，这时可改用 `videoBitrate`。照片、实拍素材较多时可以试试 `hardware=true`，并用读取器回读检查画质。

### 帧数据

`writer.writeFrame(frame,repeatCount)` 的 `frame` 可以是：

- `gdip.bitmap`：尺寸必须与视频一致。建议用 PARGB 格式（`0xE200B`），这时只是一次内存复制；其他格式由 GDI+ 自动转换。半透明的部分按黑底合成。
- buffer、字符串或指针：自上而下、字节序 B,G,R,X、每行 `宽×4` 字节。

`repeatCount` 表示把同一帧连续写入多次（共用同一份数据），适合静止画面。时间戳按帧号自动计算，长时间写入也不会漂移。

### 音频数据

`writer.writeAudio(pcm)` 写入 int16 交错 PCM（立体声为 L,R,L,R…），字节数必须是 `声道数×2` 的整数倍。可以分块多次写入，时间戳按已写入的采样数自动计算。

音视频要交替写入：如果先写完全部画面再写声音，系统会在内存里缓存整段视频，长视频会耗尽 32 位进程的内存。`writer.videoTime` 与 `writer.audioTime` 记录了两路已写入的时长，可以据此对齐。

## 读取与解码

```aardio
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();
```

- `reader.info`：`duration`（秒）、`video`（`width`、`height`、`fps`、`frameRate`、`subtype`、`bitrate`）、`audio`（`sampleRate`、`channels`、`bitsPerSample`、`subtype`、`bitrate`）。没有视频或音频时对应字段为 `null`。
- `reader.readFrame()`：返回 `位图,时间,帧号`，帧号从 0 开始、按时间戳换算；读完返回 `null`，出错返回 `null,错误信息`。`eachFrame()` 迭代中遇到的错误记录在 `reader.lastError`。
- `reader.readAudio(seconds)`：解码为 int16 PCM，返回 `PCM,起始时间`。指定秒数时按解码块边界读取（会略多一点），适合分段处理长音频；读完返回 `null`。解码后的采样率和声道数见 `reader.info.audio`。
- 输出格式：`sys.mediaFoundation.reader(path,{sampleRate=48000;channels=2})` 让系统解码器直接重采样、混缩（MP3、AAC 等任意采样率都能统一到 48k）。省略时与源相同；多于 2 声道的源默认混缩为立体声。
- `reader.seek(seconds)`：视频与音频同时定位。之后读到的首帧是离该时刻最近的帧，音频从该时刻开始。

视频和音频各用一个系统读取器，读取位置互不影响，只读其中一路时也不会在内存里堆积另一路的数据。

## 颜色、时间与性能

以下行为已在 Windows 11 上用库自带的验收测试实测确认，并用 Chromium（WebView2）独立解码核对过。

- **颜色**：系统转换器按高度选择转换矩阵，高度大于 576 用 BT.709，否则用 BT.601。写入器会把实际使用的矩阵写进 MP4 的色彩标注（colr），有限范围 16~235。红、绿、蓝、白、黑、灰色块回读误差不超过 4（0~255）。
- **时间**：写入器关闭了 B 帧，首帧从 0 秒开始，不丢帧、不重复。音频起音与画面对齐（系统解码器与 Chromium 实测偏差均为 0 ms）。AAC 以 1024 个采样为一帧，文件时长可能比画面长不到一帧音频（约 21 ms 以内）。
- **性能**（软件编码，含简单的 GDI+ 绘制与音频合成）：720p30 写入约 3 ms/帧、读取约 1.5 ms/帧；1080p30 写入约 5.5 ms/帧、读取约 3 ms/帧。画面复杂时绘制本身往往更慢（例如逐帧缩放 4K 照片），编码与绘制并行。

## 注意事项

- 写入器必须调用 `close()`，否则 MP4 文件不完整（对象被回收时也会自动调用，但不要依赖它）。
- aardio 是 32 位进程：画一帧写一帧，音频分块写入，不要在内存里累积整片。
- 渲染整片视频可能需要几秒到几分钟，放在后台线程里运行即可（Autos 的工具调用本身就在后台线程）。写入器和读取器只能在创建它们的线程里使用，在线程内 `import` 并创建对象。
- H.264 要求宽高为偶数。
- 出错时函数返回 `null,错误信息`，错误信息里包含 HRESULT 错误码。

## 底层接口

`sys.mediaFoundation` 提供了 `IMFAttributes`、`IMFMediaType`、`IMFSample`、`IMFMediaBuffer`、`IMF2DBuffer`、`IMFSinkWriter`、`IMFSourceReader` 接口声明（方法顺序按 Windows SDK 头文件）、常用 GUID 表 `guids`，以及按名称批量设置属性的函数：

```aardio
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` 接口对象，可用于库没有封装的高级操作。
