Skip to content

播放音视频

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

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

先约定 stream_id

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

  • 取值范围是 015
  • 同一条连接内,音频和视频不能共用同一个 stream_id
  • media 决定这一帧是音频还是视频,stream_id 只决定它属于哪一路流。

如果你使用 CLI 工具启动本机设备端示例,它默认使用:

用途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;
    }
}

本文 C 示例是连接建立后开始送流;如果你希望“客户端订阅后才送流”,可以实现 on_subscribe_video / on_subscribe_audio 和对应的 on_unsubscribe_*,再让支持订阅接口的客户端调用 subscribeVideo() / subscribeAudio()

客户端播放

客户端需要创建连接、音频输出和视频输出,把输出绑定到约定的 stream_id,再发起连接。下面给出各平台的写法。

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, errorCode ->
    Log.i("TiRTC", "conn state=$state errorCode=$errorCode")
  }
  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)
  conn.connect(remoteId, token)

  Log.i("TiRTC", "view=$viewCode video=$videoCode audio=$audioCode")
}

fun stopPlayback() {
  videoOutput.detach()
  audioOutput.detach()
  conn.disconnect()
}

fun releasePlayback() {
  videoOutput.detachView()
  videoOutput.release()
  audioOutput.release()
  conn.release()
}
ts
import {
  TiRtcAudioOutput,
  TiRtcAudioOutputState,
  TiRtcConn,
  TiRtcConnState,
  TiRtcVideoFit,
  TiRtcVideoOutput,
  TiRtcVideoOutputState,
  TiRtcVideoOutputView,
} from 'tirtc-av/Index';

const conn = new TiRtcConn();
const audioOutput = new TiRtcAudioOutput();
const videoOutput = new TiRtcVideoOutput();

@Builder
function RemoteVideoView() {
  TiRtcVideoOutputView({
    output: videoOutput,
    fit: TiRtcVideoFit.contain,
    width: '100%',
    height: '100%',
  });
}

function startPlayback(remoteId: string, token: string): void {
  conn.onStateChanged = (state: TiRtcConnState, errorCode: number): void => {
    console.info(`conn state=${state} error=${errorCode}`);
  };
  audioOutput.onStateChanged = (state: TiRtcAudioOutputState): void => {
    console.info(`audio state=${state}`);
  };
  videoOutput.onStateChanged = (state: TiRtcVideoOutputState): void => {
    console.info(`video state=${state}`);
  };
  videoOutput.onRenderSizeChanged = (size): void => {
    console.info(`video size=${size.width}x${size.height}`);
  };

  conn.connect({ remoteId, token });
  const audioCode = audioOutput.attach({ connection: conn, streamId: 10 });
  const videoCode = videoOutput.attach({ connection: conn, streamId: 11 });

  console.info(`audio=${audioCode} video=${videoCode}`);
}

function stopPlayback(): void {
  videoOutput.detach();
  audioOutput.detach();
  conn.disconnect();
}

function disposePlayback(): void {
  videoOutput.dispose();
  audioOutput.dispose();
  conn.dispose();
}
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)
        conn.connect(remoteId: remoteId, token: token)

        print("view=\(viewCode) audio=\(audioCode) video=\(videoCode)")
    }

    func stop() {
        videoOutput.detach()
        videoOutput.detachView()
        audioOutput.detach()
        conn.disconnect()
    }

    func dispose() {
        videoOutput.invalidate()
        audioOutput.invalidate()
        conn.invalidate()
    }

    func conn(_ conn: TiRtcConn, didReceiveCommand commandId: UInt32, data: Data) {}

    func conn(_ conn: TiRtcConn, didChangeState state: TiRtcConnState, errorCode: Int32) {
        print("conn state=\(state.rawValue) errorCode=\(errorCode)")
    }

    func conn(_ conn: TiRtcConn, didReceiveStreamMessage streamId: UInt8, timestampMs: UInt32, data: Data) {}

    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)")
    }
}
js
const appId = 'your-app-id';

TiRtc.initialize(TiRtcInitOptions({ appId }));
const readyPromise = TiRtc.videoOutputReady();

const conn = new TiRtcConn();
const audioOutput = TiRtcAudioOutput({ connection: conn, streamId: 10 });
const videoOutput = TiRtcVideoOutput({ connection: conn, streamId: 11 });

async function startPlayback({
  deviceId,
  token,
}) {
  // 首次播放前,先等待视频依赖准备就绪。
  await readyPromise;

  await conn.connect({ deviceId, token });

  audioOutput.attach();
  videoOutput.attach();

  // 请求远端开始发送约定的音视频流。
  conn.subscribeAudio({ streamId: 10 });
  conn.subscribeVideo({ streamId: 11 });
}

function stopPlayback() {
  videoOutput.detach();
  audioOutput.detach();
  conn.disconnect();
}

成功信号

  • 连接进入 connected / CONNECTED
  • 音频输出进入 playing / PLAYING
  • 视频输出进入 rendering / RENDERING,或收到视频尺寸变化回调。

如果已经连接但没有画面,先确认三件事:

  • 客户端绑定的 stream_id 是否和设备端发送一致;
  • 设备端视频第一帧是否是关键帧;
  • 客户端是否已经把视频输出挂到真实可见的视图上。

TiRTC 开发文档