520 lines
20 KiB
Rust
520 lines
20 KiB
Rust
//! 流实例 — 发布者 + 分发器 + GOP 缓存 + 订阅者列表
|
||
//!
|
||
/// Stream 是引擎的核心数据聚合单元,对应 lal 中的 "Group" 概念:
|
||
/// - 每个流路径(如 "live/test")对应一个 Stream 实例
|
||
/// - 一个 Stream 拥有恰好一个 Publisher
|
||
/// - 一个 Stream 可以有 N 个 Subscriber
|
||
/// - 内置 GOP 缓存,确保新订阅者能从最近关键帧开始播放
|
||
|
||
use std::sync::atomic::{AtomicU64, Ordering};
|
||
use std::sync::{Arc, Mutex};
|
||
|
||
use crate::core::dispatcher::Dispatcher;
|
||
use crate::core::publisher::Publisher;
|
||
use crate::core::subscriber::Subscriber;
|
||
use crate::sdk::types::{AudioCodec, AVFrame, CodecExtraInfo, StreamCodecMeta, StreamPath, StreamSummary, VideoCodec};
|
||
|
||
/// 流实例
|
||
///
|
||
/// 聚合了发布者、分发器、GOP 缓存等核心组件
|
||
pub struct Stream {
|
||
/// 流路径
|
||
path: StreamPath,
|
||
/// 发布者
|
||
publisher: Arc<Publisher>,
|
||
/// 分发器
|
||
dispatcher: Arc<Dispatcher>,
|
||
/// GOP 缓存:保存最近一个完整 GOP(关键帧 + 后续 delta 帧)
|
||
///
|
||
/// 新订阅者连接时,先将缓存中的帧推送给它,
|
||
/// 确保播放端能立即从关键帧开始解码
|
||
gop_cache: Mutex<Vec<AVFrame>>,
|
||
/// 已检测到的视频编解码器
|
||
video_codec: std::sync::atomic::AtomicU8,
|
||
/// 已检测到的音频编解码器
|
||
audio_codec: std::sync::atomic::AtomicU8,
|
||
/// 最近视频帧时间戳(毫秒)
|
||
last_video_ts: AtomicU64,
|
||
/// 最近音频帧时间戳(毫秒);用 u64::MAX 表示"尚无"
|
||
last_audio_ts: AtomicU64,
|
||
/// 已接收总视频帧数
|
||
total_video_frames: AtomicU64,
|
||
/// 已接收总音频帧数
|
||
total_audio_frames: AtomicU64,
|
||
}
|
||
|
||
/// video_codec 原子存储编码
|
||
const VC_UNKNOWN: u8 = 0;
|
||
const VC_H264: u8 = 1;
|
||
const VC_H265: u8 = 2;
|
||
|
||
/// audio_codec 原子存储编码
|
||
const AC_UNKNOWN: u8 = 0;
|
||
const AC_AAC: u8 = 1;
|
||
const AC_G711A: u8 = 2;
|
||
const AC_G711U: u8 = 3;
|
||
const AC_OPUS: u8 = 4;
|
||
|
||
const NO_TS: u64 = u64::MAX;
|
||
|
||
impl Stream {
|
||
/// 创建流实例
|
||
///
|
||
/// 自动创建关联的 Publisher 和 Dispatcher
|
||
pub fn new(path: StreamPath) -> Self {
|
||
let publisher = Arc::new(Publisher::new(path.clone()));
|
||
let dispatcher = Arc::new(Dispatcher::new(publisher.clone()));
|
||
Self {
|
||
path,
|
||
publisher,
|
||
dispatcher,
|
||
gop_cache: Mutex::new(Vec::new()),
|
||
video_codec: std::sync::atomic::AtomicU8::new(VC_UNKNOWN),
|
||
audio_codec: std::sync::atomic::AtomicU8::new(AC_UNKNOWN),
|
||
last_video_ts: AtomicU64::new(NO_TS),
|
||
last_audio_ts: AtomicU64::new(NO_TS),
|
||
total_video_frames: AtomicU64::new(0),
|
||
total_audio_frames: AtomicU64::new(0),
|
||
}
|
||
}
|
||
|
||
/// 获取发布者引用
|
||
pub fn publisher(&self) -> Arc<Publisher> {
|
||
self.publisher.clone()
|
||
}
|
||
|
||
/// 创建订阅者并加入分发器
|
||
///
|
||
/// 订阅时会先将 GOP 缓存中的帧推送给新订阅者,
|
||
/// 然后将订阅者注册到 Dispatcher 以接收后续帧
|
||
pub fn subscribe(&self) -> Arc<Subscriber> {
|
||
let sub = Arc::new(Subscriber::new(self.path.clone()));
|
||
// 将 GOP 缓存中的帧推送给新订阅者
|
||
{
|
||
let cache = self.gop_cache.lock().unwrap();
|
||
for frame in cache.iter() {
|
||
sub.push_frame(frame.clone());
|
||
}
|
||
}
|
||
self.dispatcher.add_subscriber(sub.clone());
|
||
sub
|
||
}
|
||
|
||
/// 分发一帧到所有订阅者
|
||
///
|
||
/// 同时维护 GOP 缓存:
|
||
/// - 遇到新关键帧时清空旧缓存,开始新 GOP
|
||
/// - 非关键视频帧追加到当前 GOP
|
||
/// - 音频帧不纳入 GOP 缓存
|
||
pub fn dispatch_frame(&self, frame: AVFrame) {
|
||
// 更新统计信息
|
||
if frame.is_video() {
|
||
self.total_video_frames.fetch_add(1, Ordering::Relaxed);
|
||
self.last_video_ts.store(frame.timestamp_ms, Ordering::Relaxed);
|
||
let vc = match frame.video_codec {
|
||
VideoCodec::H264 => VC_H264,
|
||
VideoCodec::H265 => VC_H265,
|
||
_ => VC_UNKNOWN,
|
||
};
|
||
self.video_codec.store(vc, Ordering::Relaxed);
|
||
} else if frame.is_audio() {
|
||
self.total_audio_frames.fetch_add(1, Ordering::Relaxed);
|
||
self.last_audio_ts.store(frame.timestamp_ms, Ordering::Relaxed);
|
||
let ac = match frame.audio_codec {
|
||
AudioCodec::Aac => AC_AAC,
|
||
AudioCodec::G711A => AC_G711A,
|
||
AudioCodec::G711U => AC_G711U,
|
||
AudioCodec::Opus => AC_OPUS,
|
||
_ => AC_UNKNOWN,
|
||
};
|
||
self.audio_codec.store(ac, Ordering::Relaxed);
|
||
}
|
||
{
|
||
let mut cache = self.gop_cache.lock().unwrap();
|
||
let is_video_seq = frame.is_video() && frame.is_seq_header();
|
||
let is_audio_seq = frame.is_audio() && frame.is_seq_header();
|
||
let is_idr = frame.is_video() && frame.is_keyframe() && !frame.is_seq_header();
|
||
let is_pframe = frame.is_video() && !frame.is_keyframe();
|
||
|
||
if is_idr {
|
||
// 保留 seq header,清空其余
|
||
let seq_headers: Vec<AVFrame> = cache.iter()
|
||
.filter(|f| f.is_seq_header())
|
||
.cloned()
|
||
.collect();
|
||
cache.clear();
|
||
cache.extend(seq_headers);
|
||
cache.push(frame.clone());
|
||
} else if is_pframe {
|
||
cache.push(frame.clone());
|
||
} else if is_video_seq {
|
||
// 替换已有的视频 seq header 或插入
|
||
let has_video_seq = cache.iter().any(|f| f.is_video() && f.is_seq_header());
|
||
if has_video_seq {
|
||
cache.retain(|f| !(f.is_video() && f.is_seq_header()));
|
||
}
|
||
cache.insert(0, frame.clone());
|
||
} else if is_audio_seq {
|
||
let has_audio_seq = cache.iter().any(|f| f.is_audio() && f.is_seq_header());
|
||
if has_audio_seq {
|
||
cache.retain(|f| !(f.is_audio() && f.is_seq_header()));
|
||
}
|
||
cache.push(frame.clone());
|
||
}
|
||
}
|
||
self.dispatcher.dispatch_frame(frame);
|
||
}
|
||
|
||
/// 获取当前订阅者数量
|
||
pub fn subscriber_count(&self) -> usize {
|
||
self.dispatcher.subscriber_count()
|
||
}
|
||
|
||
/// 获取流路径引用
|
||
pub fn path(&self) -> &StreamPath {
|
||
&self.path
|
||
}
|
||
|
||
/// 获取流的摘要信息快照
|
||
pub fn summary(&self) -> StreamSummary {
|
||
let vc = match self.video_codec.load(Ordering::Relaxed) {
|
||
VC_H264 => VideoCodec::H264,
|
||
VC_H265 => VideoCodec::H265,
|
||
_ => VideoCodec::Unknown,
|
||
};
|
||
let ac = match self.audio_codec.load(Ordering::Relaxed) {
|
||
AC_AAC => AudioCodec::Aac,
|
||
AC_G711A => AudioCodec::G711A,
|
||
AC_G711U => AudioCodec::G711U,
|
||
AC_OPUS => AudioCodec::Opus,
|
||
_ => AudioCodec::Unknown,
|
||
};
|
||
let vts = self.last_video_ts.load(Ordering::Relaxed);
|
||
let ats = self.last_audio_ts.load(Ordering::Relaxed);
|
||
StreamSummary {
|
||
path: self.path.clone(),
|
||
video_codec: vc,
|
||
audio_codec: ac,
|
||
subscriber_count: self.subscriber_count(),
|
||
last_video_timestamp_ms: if vts == NO_TS { None } else { Some(vts) },
|
||
last_audio_timestamp_ms: if ats == NO_TS { None } else { Some(ats) },
|
||
gop_cache_size: self.gop_cache.lock().unwrap().len(),
|
||
total_video_frames: self.total_video_frames.load(Ordering::Relaxed),
|
||
total_audio_frames: self.total_audio_frames.load(Ordering::Relaxed),
|
||
}
|
||
}
|
||
|
||
/// 获取流的编解码器元数据
|
||
///
|
||
/// 扫描 GOP 缓存中的 seq_header 帧,提取 H.264 SPS/PPS 和 AAC AudioSpecificConfig。
|
||
/// 用于 RTSP DESCRIBE 响应生成 SDP 描述。
|
||
///
|
||
/// AAC AudioSpecificConfig 2 字节解析:
|
||
/// - bits [4:0] byte0 + [7:5] byte1 → audioObjectType(5 bits 从 byte0 高 5 位)
|
||
/// - bits [4:1] byte1 → samplingFrequencyIndex(4 bits)
|
||
/// - bit [0] byte1 + [7:5] byte2 → channelConfiguration(4 bits)
|
||
pub fn codec_metadata(&self) -> StreamCodecMeta {
|
||
let cache = self.gop_cache.lock().unwrap();
|
||
let mut meta = StreamCodecMeta::default();
|
||
|
||
for frame in cache.iter() {
|
||
if let Some(ref info) = frame.codec_info {
|
||
match info {
|
||
CodecExtraInfo::H264SeqHeader(sh) => {
|
||
meta.h264_sps = Some(sh.sps.clone());
|
||
meta.h264_pps = Some(sh.pps.clone());
|
||
}
|
||
CodecExtraInfo::AacSeqHeader(ah) => {
|
||
meta.aac_config = Some(ah.audio_specific_config.clone());
|
||
// 解析 2 字节 AudioSpecificConfig 提取采样率和通道数
|
||
let config = &ah.audio_specific_config;
|
||
if config.len() >= 2 {
|
||
// samplingFrequencyIndex: bits [7:3] of byte[1]
|
||
// 格式: audioObjectType(5)<<11 | samplingFreqIndex(4)<<7 | channelConfig(3)<<4 | ...
|
||
// 实际 layout: byte0[7:3]=audioObjectType(5), byte0[2:0]+byte1[7]=samplingFreqIndex(4)
|
||
// byte1[6:3]=channelConfiguration(4), ...
|
||
// 简化:byte0 低 3 位 << 1 | byte1 高 1 位 = samplingFreqIndex
|
||
let sfi = (((config[0] & 0x07) as u32) << 1)
|
||
| (((config[1] >> 7) & 0x01) as u32);
|
||
// channelConfiguration: byte1 bits [6:3]
|
||
let ch = (config[1] >> 3) & 0x0F;
|
||
meta.audio_sample_rate = sampling_rate_from_index(sfi);
|
||
meta.audio_channels = ch;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
meta
|
||
}
|
||
}
|
||
|
||
/// 根据 samplingFrequencyIndex 查表返回采样率
|
||
///
|
||
/// AAC AudioSpecificConfig 中的采样率索引表(ISO 14496-3 Table 1.16)
|
||
fn sampling_rate_from_index(index: u32) -> u32 {
|
||
match index {
|
||
0 => 96000,
|
||
1 => 88200,
|
||
2 => 64000,
|
||
3 => 48000,
|
||
4 => 44100,
|
||
5 => 32000,
|
||
6 => 24000,
|
||
7 => 22050,
|
||
8 => 16000,
|
||
9 => 12000,
|
||
10 => 11025,
|
||
11 => 8000,
|
||
12 => 7350,
|
||
_ => 44100, // 未知时默认 44100
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use crate::sdk::traits::{PublisherApi, SubscriberApi};
|
||
use crate::sdk::types::{AudioCodec, CodecExtraInfo, FrameType, VideoCodec, AacSeqHeader, H264SeqHeader};
|
||
use std::sync::Arc;
|
||
|
||
fn make_path() -> StreamPath {
|
||
StreamPath::new("live", "stream")
|
||
}
|
||
|
||
fn make_keyframe(ts: u64) -> AVFrame {
|
||
AVFrame::new_video(ts, Arc::new(vec![1]), VideoCodec::H264, FrameType::KeyFrame)
|
||
}
|
||
|
||
fn make_interframe(ts: u64) -> AVFrame {
|
||
AVFrame::new_video(ts, Arc::new(vec![2]), VideoCodec::H264, FrameType::InterFrame)
|
||
}
|
||
|
||
#[test]
|
||
fn test_stream_new_path_correct() {
|
||
let stream = Stream::new(make_path());
|
||
assert_eq!(stream.path().full_path(), "live/stream");
|
||
}
|
||
|
||
#[test]
|
||
fn test_stream_publisher_is_active() {
|
||
let stream = Stream::new(make_path());
|
||
assert!(stream.publisher().is_active());
|
||
}
|
||
|
||
#[test]
|
||
fn test_stream_subscribe_gets_empty_gop_cache() {
|
||
let stream = Stream::new(make_path());
|
||
let sub = stream.subscribe();
|
||
// 尚未写入任何帧,GOP 缓存为空
|
||
assert!(sub.read_frame().is_none());
|
||
assert_eq!(stream.subscriber_count(), 1);
|
||
}
|
||
|
||
#[test]
|
||
fn test_stream_dispatch_to_subscriber() {
|
||
let stream = Stream::new(make_path());
|
||
let sub = stream.subscribe();
|
||
stream.dispatch_frame(make_keyframe(100));
|
||
let f = sub.read_frame().unwrap();
|
||
assert_eq!(f.timestamp_ms, 100);
|
||
assert!(f.is_keyframe());
|
||
}
|
||
|
||
#[test]
|
||
fn test_stream_gop_cache_new_subscriber_gets_cached_frames() {
|
||
let stream = Stream::new(make_path());
|
||
|
||
// 写入一个 GOP: 关键帧 + 2 个 delta 帧
|
||
stream.dispatch_frame(make_keyframe(100));
|
||
stream.dispatch_frame(make_interframe(200));
|
||
stream.dispatch_frame(make_interframe(300));
|
||
|
||
// 新订阅者应该收到缓存的 GOP
|
||
let sub = stream.subscribe();
|
||
let f1 = sub.read_frame().unwrap();
|
||
assert_eq!(f1.timestamp_ms, 100);
|
||
assert!(f1.is_keyframe());
|
||
assert_eq!(sub.read_frame().unwrap().timestamp_ms, 200);
|
||
assert_eq!(sub.read_frame().unwrap().timestamp_ms, 300);
|
||
}
|
||
|
||
#[test]
|
||
fn test_stream_gop_cache_cleared_on_new_keyframe() {
|
||
let stream = Stream::new(make_path());
|
||
|
||
// 第一个 GOP
|
||
stream.dispatch_frame(make_keyframe(100));
|
||
stream.dispatch_frame(make_interframe(200));
|
||
|
||
// 新关键帧清空旧缓存
|
||
stream.dispatch_frame(make_keyframe(300));
|
||
stream.dispatch_frame(make_interframe(400));
|
||
|
||
// 新订阅者只应收到第二个 GOP
|
||
let sub = stream.subscribe();
|
||
assert_eq!(sub.read_frame().unwrap().timestamp_ms, 300);
|
||
assert_eq!(sub.read_frame().unwrap().timestamp_ms, 400);
|
||
assert!(sub.read_frame().is_none());
|
||
}
|
||
|
||
#[test]
|
||
fn test_stream_multiple_subscribers_all_receive_frames() {
|
||
let stream = Stream::new(make_path());
|
||
let sub1 = stream.subscribe();
|
||
let sub2 = stream.subscribe();
|
||
|
||
stream.dispatch_frame(make_keyframe(50));
|
||
stream.dispatch_frame(make_interframe(60));
|
||
|
||
assert_eq!(sub1.read_frame().unwrap().timestamp_ms, 50);
|
||
assert_eq!(sub1.read_frame().unwrap().timestamp_ms, 60);
|
||
assert_eq!(sub2.read_frame().unwrap().timestamp_ms, 50);
|
||
assert_eq!(sub2.read_frame().unwrap().timestamp_ms, 60);
|
||
}
|
||
|
||
#[test]
|
||
fn test_stream_summary_initial_state() {
|
||
let stream = Stream::new(make_path());
|
||
let s = stream.summary();
|
||
assert_eq!(s.path.full_path(), "live/stream");
|
||
assert_eq!(s.video_codec, VideoCodec::Unknown);
|
||
assert_eq!(s.audio_codec, AudioCodec::Unknown);
|
||
assert_eq!(s.subscriber_count, 0);
|
||
assert!(s.last_video_timestamp_ms.is_none());
|
||
assert!(s.last_audio_timestamp_ms.is_none());
|
||
assert_eq!(s.total_video_frames, 0);
|
||
assert_eq!(s.total_audio_frames, 0);
|
||
}
|
||
|
||
#[test]
|
||
fn test_stream_summary_tracks_codecs_and_counts() {
|
||
let stream = Stream::new(make_path());
|
||
stream.dispatch_frame(make_keyframe(100));
|
||
stream.dispatch_frame(AVFrame::new_audio(100, Arc::new(vec![0xAF]), AudioCodec::Aac));
|
||
stream.dispatch_frame(make_interframe(133));
|
||
|
||
let s = stream.summary();
|
||
assert_eq!(s.video_codec, VideoCodec::H264);
|
||
assert_eq!(s.audio_codec, AudioCodec::Aac);
|
||
assert_eq!(s.total_video_frames, 2);
|
||
assert_eq!(s.total_audio_frames, 1);
|
||
assert_eq!(s.last_video_timestamp_ms, Some(133));
|
||
assert_eq!(s.last_audio_timestamp_ms, Some(100));
|
||
assert!(s.gop_cache_size > 0);
|
||
}
|
||
|
||
#[test]
|
||
fn test_stream_summary_subscriber_count() {
|
||
let stream = Stream::new(make_path());
|
||
assert_eq!(stream.summary().subscriber_count, 0);
|
||
let _sub1 = stream.subscribe();
|
||
assert_eq!(stream.summary().subscriber_count, 1);
|
||
let _sub2 = stream.subscribe();
|
||
assert_eq!(stream.summary().subscriber_count, 2);
|
||
}
|
||
|
||
/// 测试:codec_metadata 初始状态返回空元数据
|
||
#[test]
|
||
fn test_stream_codec_metadata_initial_state_empty() {
|
||
let stream = Stream::new(make_path());
|
||
let meta = stream.codec_metadata();
|
||
assert!(meta.h264_sps.is_none(), "初始状态不应有 SPS");
|
||
assert!(meta.h264_pps.is_none(), "初始状态不应有 PPS");
|
||
assert!(meta.aac_config.is_none(), "初始状态不应有 AAC config");
|
||
assert_eq!(meta.audio_sample_rate, 0);
|
||
assert_eq!(meta.audio_channels, 0);
|
||
}
|
||
|
||
/// 测试:codec_metadata 从 GOP 缓存中的 seq_header 帧提取 H.264 SPS/PPS
|
||
#[test]
|
||
fn test_stream_codec_metadata_extracts_h264_sps_pps() {
|
||
let stream = Stream::new(make_path());
|
||
let sps_data = Arc::new(vec![0x67, 0x64, 0x00, 0x29, 0xAC, 0xD9, 0x40, 0x78, 0x02]);
|
||
let pps_data = Arc::new(vec![0x68, 0xEE, 0x31, 0x12]);
|
||
|
||
let mut frame = AVFrame::new_video(0, Arc::new(vec![]), VideoCodec::H264, FrameType::KeyFrame);
|
||
frame.codec_info = Some(CodecExtraInfo::H264SeqHeader(H264SeqHeader {
|
||
sps: sps_data.clone(),
|
||
pps: pps_data.clone(),
|
||
}));
|
||
stream.dispatch_frame(frame);
|
||
|
||
let meta = stream.codec_metadata();
|
||
assert!(meta.h264_sps.is_some(), "应有 SPS");
|
||
assert!(meta.h264_pps.is_some(), "应有 PPS");
|
||
assert_eq!(&*meta.h264_sps.unwrap(), &*sps_data);
|
||
assert_eq!(&*meta.h264_pps.unwrap(), &*pps_data);
|
||
}
|
||
|
||
/// 测试:codec_metadata 从 GOP 缓存中的 seq_header 帧提取 AAC config
|
||
#[test]
|
||
fn test_stream_codec_metadata_extracts_aac_config() {
|
||
let stream = Stream::new(make_path());
|
||
// AAC AudioSpecificConfig: audioObjectType=2(AAC-LC), samplingFreqIndex=3(48000), channelConfig=2
|
||
// byte0 = (audioObjectType=2) << 3 | (sfi=3) >> 1 = 00010_001 = 0x11
|
||
// byte1 = (sfi=3 & 1) << 7 | (channelConfig=2) << 3 = 1_0010_000 = 0x90
|
||
let aac_config = Arc::new(vec![0x11, 0x90]);
|
||
|
||
let mut frame = AVFrame::new_audio(0, Arc::new(vec![]), AudioCodec::Aac);
|
||
frame.codec_info = Some(CodecExtraInfo::AacSeqHeader(AacSeqHeader {
|
||
audio_specific_config: aac_config.clone(),
|
||
}));
|
||
stream.dispatch_frame(frame);
|
||
|
||
let meta = stream.codec_metadata();
|
||
assert!(meta.aac_config.is_some(), "应有 AAC config");
|
||
assert_eq!(&*meta.aac_config.unwrap(), &*aac_config);
|
||
assert_eq!(meta.audio_sample_rate, 48000, "采样率应为 48000");
|
||
assert_eq!(meta.audio_channels, 2, "通道数应为 2");
|
||
}
|
||
|
||
/// 测试:codec_metadata 同时包含视频和音频元数据
|
||
#[test]
|
||
fn test_stream_codec_metadata_both_video_and_audio() {
|
||
let stream = Stream::new(make_path());
|
||
let sps_data = Arc::new(vec![0x67, 0x42, 0xC0, 0x1E, 0xD9]);
|
||
let pps_data = Arc::new(vec![0x68, 0xCE, 0x38, 0x80]);
|
||
// AAC AudioSpecificConfig: samplingFreqIndex=4(44100), channelConfig=2
|
||
// byte0 = (2<<3) | (4>>1) = 0x12, byte1 = (4<<7) | (2<<3) = 0x90... wait
|
||
// audioObjectType=2(5bits) = 00010
|
||
// samplingFreqIndex=4(4bits) = 0100
|
||
// channelConfiguration=2(4bits) = 0010
|
||
// layout: [00010][0100][0010][000]
|
||
// byte0 = 00010_010 = 0x12
|
||
// byte1 = 0_0010_000 = 0x10
|
||
let aac_config = Arc::new(vec![0x12, 0x10]);
|
||
|
||
let mut video_frame = AVFrame::new_video(0, Arc::new(vec![]), VideoCodec::H264, FrameType::KeyFrame);
|
||
video_frame.codec_info = Some(CodecExtraInfo::H264SeqHeader(H264SeqHeader {
|
||
sps: sps_data.clone(),
|
||
pps: pps_data.clone(),
|
||
}));
|
||
stream.dispatch_frame(video_frame);
|
||
|
||
let mut audio_frame = AVFrame::new_audio(0, Arc::new(vec![]), AudioCodec::Aac);
|
||
audio_frame.codec_info = Some(CodecExtraInfo::AacSeqHeader(AacSeqHeader {
|
||
audio_specific_config: aac_config.clone(),
|
||
}));
|
||
stream.dispatch_frame(audio_frame);
|
||
|
||
let meta = stream.codec_metadata();
|
||
assert!(meta.h264_sps.is_some());
|
||
assert!(meta.h264_pps.is_some());
|
||
assert!(meta.aac_config.is_some());
|
||
assert_eq!(meta.audio_sample_rate, 44100, "采样率应为 44100");
|
||
assert_eq!(meta.audio_channels, 2, "通道数应为 2");
|
||
}
|
||
|
||
/// 测试:sampling_rate_from_index 查表正确
|
||
#[test]
|
||
fn test_sampling_rate_from_index_known_values() {
|
||
assert_eq!(super::sampling_rate_from_index(0), 96000);
|
||
assert_eq!(super::sampling_rate_from_index(3), 48000);
|
||
assert_eq!(super::sampling_rate_from_index(4), 44100);
|
||
assert_eq!(super::sampling_rate_from_index(11), 8000);
|
||
assert_eq!(super::sampling_rate_from_index(15), 44100); // 未知索引默认 44100
|
||
}
|
||
}
|