Skip to content

播放音视频

这页说明一条连接建立后,设备端如何按 stream_id 发送音视频,客户端如何把同一组 stream_id 绑定到音频和视频输出。

如果你还没完成设备启动或客户端建连,先看集成到设备端集成到客户端连接设备端

先约定 stream_id

stream_id 标识同一条连接里的某一路媒体流。C SDK 当前约束是:

  • 取值范围是 015
  • 同一条连接内,音频和视频不能共用同一个 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 是否和设备端发送一致;
  • 设备端视频第一帧是否是关键帧;
  • 客户端是否已经把视频输出挂到真实可见的视图上。

TiRTC 开发文档