播放音视频
这页说明一条连接建立后,设备端如何按 stream_id 发送音视频,客户端如何把同一组 stream_id 绑定到音频和视频输出。
如果你还没完成设备启动或客户端建连,先看集成到设备端、集成到客户端 和连接设备端。
先约定 stream_id
stream_id 标识同一条连接里的某一路媒体流。C SDK 当前约束是:
- 取值范围是
0到15。 - 同一条连接内,音频和视频不能共用同一个
stream_id。 media决定这一帧是音频还是视频,stream_id只决定它属于哪一路流。
标准设备端 Demo 使用:
| 用途 | stream_id |
|---|---|
| 音频 | 10 |
| 视频 | 11 |
下文沿用这组编号。你也可以改成自己的编号,只要设备端发送和客户端绑定保持一致。
设备端发送音视频
设备端拿到连接句柄 hconn 后,在自己的采集或编码线程里填充 TIRTCFRAMEINFO,再调用 TiRtcSendAudioStream() / TiRtcSendVideoStream()。
c
#include <stdint.h>
#include <string.h>
#include "tiRTC.h"
static const uint8_t kAudioStreamId = 10;
static const uint8_t kVideoStreamId = 11;
int send_audio(tirtc_conn_t hconn, const void *data, uint32_t len, uint32_t ts_ms)
{
TIRTCFRAMEINFO fi;
memset(&fi, 0, sizeof(fi));
fi.stream_id = kAudioStreamId;
fi.media = TIRTC_AUDIO_ALAW;
fi.flags = TIRTC_AUDIOSAMPLE_8K16B1C;
fi.ts = ts_ms;
fi.length = len;
return TiRtcSendAudioStream(hconn, &fi, data);
}
int send_video(tirtc_conn_t hconn, const void *data, uint32_t len, uint32_t ts_ms, int key_frame)
{
TIRTCFRAMEINFO fi;
memset(&fi, 0, sizeof(fi));
fi.stream_id = kVideoStreamId;
fi.media = TIRTC_VIDEO_H264;
fi.flags = key_frame ? TIRTC_FRAME_FLAG_KEY_FRAME : 0;
fi.ts = ts_ms;
fi.length = len;
return TiRtcSendVideoStream(hconn, &fi, data);
}视频流的第一帧必须带 TIRTC_FRAME_FLAG_KEY_FRAME。如果发送返回 TIRTC_E_BUSY,C SDK 会丢弃后续非关键帧,直到下一帧关键帧到来;你的编码线程应尽快补一个关键帧。
如果客户端请求关键帧,设备端会收到 on_request_key_frame。这个回调里只改状态,下一次编码时输出关键帧即可。
c
static volatile int g_force_key_frame = 0;
static void on_request_key_frame(tirtc_conn_t hconn, uint8_t stream_id)
{
(void)hconn;
if (stream_id == kVideoStreamId) {
g_force_key_frame = 1;
}
}标准设备端 Demo 是连接建立后开始送流;如果你希望“客户端订阅后才送流”,可以实现 on_subscribe_video / on_subscribe_audio 和对应的 on_unsubscribe_*,再让支持订阅接口的客户端调用 subscribeVideo() / subscribeAudio()。
客户端播放
客户端侧的主线是:创建连接对象和输出对象,把音频、视频输出绑定到同一条连接上的不同 stream_id,再发起连接。下面按 Flutter、Android、iOS 展示。
dart
final TiRtcConn conn = TiRtcConn();
final TiRtcAudioOutput audioOutput = TiRtcAudioOutput();
final TiRtcVideoOutput videoOutput = TiRtcVideoOutput();
Widget buildVideoView() => videoOutput.view();
void startPlayback({
required String remoteId,
required String token,
}) {
conn.onStateChanged = (TiRtcConnState state, int errorCode) {
debugPrint('conn state=$state error=$errorCode');
};
audioOutput.onStateChanged = (TiRtcAudioOutputState state) {
debugPrint('audio state=$state');
};
videoOutput.onStateChanged = (TiRtcVideoOutputState state) {
debugPrint('video state=$state');
};
videoOutput.onRenderSizeChanged = (Size size) {
debugPrint('video size=${size.width}x${size.height}');
};
conn.connect(remoteId: remoteId, token: token);
audioOutput.attach(connection: conn, streamId: 10);
videoOutput.attach(connection: conn, streamId: 11);
}
void stopPlayback() {
videoOutput.detach();
audioOutput.detach();
conn.disconnect();
}
void disposePlayback() {
videoOutput.dispose();
audioOutput.dispose();
conn.dispose();
}kotlin
val conn = TiRtcConn()
val audioOutput = TiRtcAudioOutput()
val videoOutput = TiRtcVideoOutput()
fun startPlayback(
remoteId: String,
token: String,
videoContainer: ViewGroup,
) {
conn.onStateChanged = TiRtcConnStateListener { state ->
Log.i("TiRTC", "conn state=$state")
}
conn.onDisconnected = TiRtcConnDisconnectedListener { code ->
Log.i("TiRTC", "conn disconnected code=$code")
}
audioOutput.onStateChanged = TiRtcAudioOutputStateListener { state ->
Log.i("TiRTC", "audio state=$state")
}
videoOutput.onStateChanged = TiRtcVideoOutputStateListener { state ->
Log.i("TiRTC", "video state=$state")
}
videoOutput.onRenderSizeChanged = TiRtcVideoOutputRenderSizeListener { size ->
Log.i("TiRTC", "video size=${size.width}x${size.height}")
}
val viewCode = videoOutput.attachView(videoContainer)
val videoCode = videoOutput.attach(conn, 11)
val audioCode = audioOutput.attach(conn, 10)
val connectCode = conn.connect(remoteId, token)
Log.i("TiRTC", "view=$viewCode video=$videoCode audio=$audioCode connect=$connectCode")
}
fun stopPlayback() {
videoOutput.detach()
audioOutput.detach()
conn.disconnect()
}
fun releasePlayback() {
videoOutput.detachView()
videoOutput.release()
audioOutput.release()
conn.release()
}swift
final class Player: NSObject,
TiRtcConnDelegate,
TiRtcAudioOutputDelegate,
TiRtcVideoOutputDelegate {
private lazy var conn = TiRtcConn(delegate: self)
private let audioOutput = TiRtcAudioOutput()
private let videoOutput = TiRtcVideoOutput()
func start(remoteId: String, token: String, videoView: UIView) {
audioOutput.delegate = self
videoOutput.delegate = self
let viewCode = videoOutput.attachView(videoView)
let audioCode = audioOutput.attach(to: conn, streamId: 10)
let videoCode = videoOutput.attach(to: conn, streamId: 11)
let connectCode = conn.connect(to: remoteId, token: token)
print("view=\(viewCode) audio=\(audioCode) video=\(videoCode) connect=\(connectCode)")
}
func stop() {
videoOutput.detach()
videoOutput.detachView()
audioOutput.detach()
conn.disconnect()
}
func dispose() {
videoOutput.invalidate()
audioOutput.invalidate()
conn.invalidate()
}
func conn(_ conn: TiRtcConn, didChangeState state: TiRtcConnState) {
print("conn state=\(state.rawValue)")
}
func conn(_ conn: TiRtcConn, didDisconnectWithCode code: Int32) {
print("conn disconnected code=\(code)")
}
func conn(_ conn: TiRtcConn, didReceiveCommand commandId: UInt32, data: Data) {}
func conn(_ conn: TiRtcConn, didFailWithCode code: Int32, message: String) {
print("conn error=\(code) message=\(message)")
}
func conn(_ conn: TiRtcConn, didReceiveStreamMessage timestampMs: UInt32, data: Data, streamId: UInt8) {}
func audioOutput(_ output: TiRtcAudioOutput, didChangeState state: TiRtcAudioOutputState) {
print("audio state=\(state.rawValue)")
}
func audioOutput(_ output: TiRtcAudioOutput, didFailWithCode code: Int32, message: String) {
print("audio error=\(code) message=\(message)")
}
func videoOutput(_ output: TiRtcVideoOutput, didChangeState state: TiRtcVideoOutputState) {
print("video state=\(state.rawValue)")
}
func videoOutput(_ output: TiRtcVideoOutput, didChangeRenderSize size: CGSize) {
print("video size=\(Int(size.width))x\(Int(size.height))")
}
func videoOutput(_ output: TiRtcVideoOutput, didFailWithCode code: Int32, message: String) {
print("video error=\(code) message=\(message)")
}
}成功信号
- 连接进入
connected/CONNECTED。 - 音频输出进入
playing/PLAYING。 - 视频输出进入
rendering/RENDERING,或收到视频尺寸变化回调。
如果已经连接但没有画面,先确认三件事:
- 客户端绑定的
stream_id是否和设备端发送一致; - 设备端视频第一帧是否是关键帧;
- 客户端是否已经把视频输出挂到真实可见的视图上。