发送和接收媒体流
本文介绍如何使用 RTC 服务端 SDK 向客户端发送媒体流并从客户端接收媒体流。
前提条件
你已经下载了最新的 RTC 服务端 SDK。详见跑通示例项目。
准备工作
在发送和接收媒体流之前,你需要完成以下准备工作。
1. 初始化 SDK
首先,你需要在你的应用程序里调用 AgoraService
和 initialize
进行全局初始化,创建并初始化 AgoraService
对象。
这个操作只需要进行一次,IAgoraService
对象的生命期和应用程序的生命期保持一致,只要应用程序没有退出,AgoraService
可以一直存在。
SDK 支持 int 型用户 ID 和 String 型用户 ID。本文仅演示 int 型用户 ID(即字符只能为数字),String 型用户 ID(字符可以是数字、字母或特殊符号) 的使用方法参考 使用 String 型用户 ID。
// Import SDK, AgoraService, and AgoraServiceConfig classes for initialization
import io.agora.rtc.SDK;
import io.agora.rtc.AgoraService;
import io.agora.rtc.AgoraServiceConfig;
// Creates an AgoraService object
AgoraService service = new AgoraService();
// Initializes the AgoraServiceConfig object
AgoraServiceConfig config = new AgoraServiceConfig();
// Enables the audio processing module
config.setEnableAudioProcessor(1);
// Disables the audio device module (Normally we do not directly connect audio capture or playback devices to a server)
config.setEnableAudioDevice(0);
// Enables video
config.setEnableVideo(1);
// Sets声网 App ID
config.setAppId(appid);
// Initializes the SDK
service.initialize(config);
2. 与 RTC 频道建立连接
初始化后,根据以下步骤与声网 RTC 频道建立连接。
-
调用
agoraRtcConnCreate
方法创建AgoraRtcConn
对象,用于与声网服务器建立连接。JavaAgoraRtcConn conn = service.agoraRtcConnCreate(null);
-
调用
registerObserver
方法监听连接事件。Javaconn.registerObserver(new ConnObserver());
-
调用
connect
与声网频道建立连接。Javaconn.connect(token, "test_channel", "1");
向客户端发送媒体流
参考以下步骤向客户端发送媒体流。
1. 创建媒体流发送器
你可以通过媒体节点工厂对象 IMediaNodeFactory
创建不同类型的媒体流发送器:
AgoraAudioPcmDataSender
:发送 PCM 格式的音频数据。AgoraVideoFrameSender
:发送 YUV 格式的视频数据。AgoraAudioEncodedFrameSender
:发送已编码的音频数据。AgoraVideoEncodedImageSender
:发送已编码的视频数据。
-
创建
AgoraMediaNodeFactory
媒体节点工厂对象。JavaAgoraMediaNodeFactory factory = service.createMediaNodeFactory();
-
根据你的需求,创建
AgoraAudioPcmDataSender
对象、AgoraVideoFrameSender
对象、AgoraAudioEncodedFrameSender
对象或AgoraVideoEncodedImageSender
对象,分别用于发送 PCM 格式的音频流、YUV 格式的视频流、已编码的音频流及已编码的视频流。Java// Creates a sender for PCM audio
AgoraAudioPcmDataSender audioFrameSender = factory.createAudioPcmDataSender();
// Creates a sender for YUV video
AgoraVideoFrameSender videoFrameSender = factory.createVideoFrameSender();
// Creates a sender for encoded audio
AgoraAudioEncodedFrameSender audioFrameSender = factory.createAudioEncodedFrameSender();
// Creates a sender for encoded video
AgoraVideoEncodedImageSender imageSender = factory.createVideoEncodedImageSender(); -
创建
AgoraLocalAudioTrack
对象和AgoraLocalVideoTrack
对象,分别对应即将发布至频道内的本地音频轨道和本地视频轨道。对象中可以使用上一步创建的发送器。Java// Creates a custom audio track that uses a PCM audio stream sender
customAudioTrack = service.createCustomAudioTrackPcm(audioFrameSender);
// Creates a custom audio track that uses an encoded audio stream sender
customAudioTrack = service.createCustomAudioTrackEncoded(audioFrameSender,0);
// Creates a custom video track that uses a YUV video stream sender
customVideoTrack = service.createCustomVideoTrackFrame(videoFrameSender);
// Creates a custom video track that uses encoded video stream sender
customVideoTrack = service.createCustomVideoTrackEncoded(videoFrameSender, option);
2. 发送媒体流
-
通过
AgoraLocalUser
对象的 publish 相关方法在声网 RTC 频道中发布上一步创建的本地音频轨道和视频轨道。Java// Enables and publishes audio and video track
customAudioTrack.setEnabled(1);
customVideoTrack.setEnabled(1);
conn.getLocalUser().publishAudio(customAudioTrack);
conn.getLocalUser().publishVideo(customVideoTrack); -
开启发送线程,调用发送器的 send 方法发送音视频数据。
JavapcmSender = new PcmSender(audioFile,audioFrameSender,numOfChannels,sampleRate);
h264Sender = new H264Sender(videoFile,1000/fps,0,0,videoFrameSender);
pcmSender.start();
h264Sender.start();本文以发送 PCM 音频数据为例进行介绍,
PcmSender
的实现如下:Java// audio thread
// send audio data every 10 ms;
class PcmSender extends FileSender {
private AgoraAudioPcmDataSender audioFrameSender;
private static final int INTERVAL = 10; //ms
private int channels;
private int samplerate;
private int bufferSize = 0;
private byte[] buffer;
public PcmSender(String filepath, AgoraAudioPcmDataSender sender,int channels,int samplerate){
super(filepath, INTERVAL);
audioFrameSender = sender;
this.channels = channels;
this.samplerate = samplerate;
this.bufferSize = channels * samplerate * 2 * INTERVAL /1000;
this.buffer = new byte[this.bufferSize];
}
// sendOneFrame calls the send method of audioFrameSender to send PCM data
@Override
public void sendOneFrame(byte[] data) {
if(data == null) return;
audioFrameSender.send(data,(int)System.currentTimeMillis(),sampleRate/(1000/INTERVAL),2,channels,samplerate);
}
@Override
public byte[] readOneFrame(FileInputStream fos) {
if(fos != null ){
try {
int size = fos.read(buffer,0,bufferSize);
if( size <= 0){
reset();
return null;
}
} catch (IOException e) {
e.printStackTrace();
}
}
return buffer;
}
}以发送 H.264 格式的编码视频为例,
H264Sender
的实现如下:Javaclass H264Sender extends FileSender {
private AgoraVideoEncodedImageSender imageSender;
private H264Reader h264Reader;
private int lastFrameType = 0;
private int height;
private int width;
private int fps;
public H264Sender(String path,int interval, int height,int width, AgoraVideoEncodedImageSender videoEncodedImageSender){
super(path,interval,false);
this.imageSender = videoEncodedImageSender;
this.h264Reader = new H264Reader(path);
this.height = height;
this.width = width;
this.fps = 1000/interval;
}
// sendOneH264Frame calls the send method of imageSender to send H.264 data.
@Override
public void sendOneFrame(byte[] data) {
if(data == null) return;
EncodedVideoFrameInfo info = new EncodedVideoFrameInfo();
long currentTime = System.currentTimeMillis();
info.setFrameType(lastFrameType);
info.setWidth(width);
info.setHeight(height);
info.setCodecType(Constants.VIDEO_CODEC_H264);
info.setCaptureTimeMs(currentTime);
info.setRenderTimeMs(currentTime);
info.setFramesPerSecond(fps);
info.setRotation(0);
imageSender.send(data,data.length,info);
}
@Override
public byte[] readOneFrame(FileInputStream fos) {
int retry = 0;
H264Reader.H264Frame frame = h264Reader.readNextFrame();
while ( frame == null && retry < 4){
h264Reader.reset();
frame = h264Reader.readNextFrame();
retry ++;
}
if( frame != null ) {
lastFrameType = frame.frameType;
return frame.data;
} else {
return null;
}
}
@Override
public void release() {
super.release();
h264Reader.close();
}
}编码视频数据的格式通过
send
方法的info
参数进行设置。
3. 断开连接并释放资源
媒体流发送完成后,你可以通过以下步骤退出频道并断开与声网服务器的连接:
-
调用
unpublish
相关方法取消发布音频流和视频流。Javaif(conn != null) {
conn.getLocalUser().unpublishAudio(customAudioTrack);
conn.getLocalUser().unpublishVideo(customVideoTrack);
} -
调用
unregisterObserver
取消连接 observer。Javaconn.unregisterObserver();
-
调用
disconnect
断开与声网服务器的连接。Javaint ret = conn.disconnect();
-
释放已创建对象的资源。
Javaconn.destroy();
从客户端接收媒体流
参考以下步骤从客户端接收媒体流。
1. 注册音视频帧 observer
实例化音视频帧 observer 对象,并通过 ILocalUserObserver
类的成员方法注册音视频帧 observer。成功注册 observer 后,SDK 会在捕捉到每个音频或视频帧时触发回调。你可以在回调中获得该音频或视频帧。
示例中的 SampleLocalUserObserver
类包含 IVideoEncodedFrameObserver
、IAudioFrameObserverBase
和 IVideoFrameObserver2
类的音视频帧 observer 对象作为成员,且继承了 ILocalUserObserver
类,可以使用其成员方法注册作为成员的音视频帧 observer 对象。
// The SampleLocalUserObserver class inherits the ILocalUserObserver class
localUserObserver = new SampleLocalUserObserver(conn.getLocalUser());
conn.getLocalUser().registerObserver(localUserObserver);
// The PcmFrameObserver class inherits the IAudioFrameObserver class
int ret = conn.getLocalUser().setPlaybackAudioFrameBeforeMixingParameters(numOfChannels, sampleRate);
if (ret > 0) {
System.out.printf("setPlaybackAudioFrameBeforeMixingParameters fail ret=%d\n", ret);
return;
}
// The H264FrameReceiver class inherits the IVideoEncodedImageReceiver class
h264FrameReceiver = new H264FrameReceiver(videoFile);
conn.getLocalUser().registerVideoEncodedFrameObserver(new AgoraVideoEncodedFrameObserver(h264FrameReceiver));
setAudioFrameObserver
和 registerVideoEncodedFrameObserver
是 SampleLocalUserObserver
类的成员方法,可用来初始化 IAudioFrameObserver
和 AgoraVideoEncodedFrameObserver
类。
localUserObserver.setAudioFrameObserver(pcmFrameObserver);
conn.getLocalUser().registerVideoEncodedFrameObserver(new AgoraVideoEncodedFrameObserver(h264FrameReceiver));
2. 接收媒体流
下面的代码展示了如何接收已编码的视频、YUV 格式的视频和 PCM 格式的音频。
// Receives encoded video by using the onEncodedVideoFrame callback and save the video data in a file
class H264FrameReceiver extends FileWriter implements IVideoEncodedFrameObserver {
public H264FrameReceiver(String path) {
super(path);
}
@Override
public int onEncodedVideoFrame(AgoraVideoEncodedFrameObserver agora_video_encoded_frame_observer, int uid, byte[] image_buffer, long length, EncodedVideoFrameInfo video_encoded_frame_info) {
System.out.println("onEncodedVideoFrame success " + video_encoded_frame_info.getFrameType());
writeData(image_buffer, (int) length);
return 1;
}
}
// Receives YUV video by using the onFrame callback and save the video data in a file
class YuvFrameObserver extends FileWriter implements IVideoFrameObserver2 {
public YuvFrameObserver(String path) {
super(path);
}
@Override
public void onFrame(AgoraVideoFrameObserver2 agora_video_frame_observer2, String channel_id, String remote_uid, VideoFrame frame) {
System.out.println("onFrame success ");
writeData(frame.getYBuffer(), frame.getYBuffer().remaining());
writeData(frame.getUBuffer(), frame.getUBuffer().remaining());
writeData(frame.getVBuffer(), frame.getVBuffer().remaining());
return ;
}
}
// Receives PCM audio by using the onPlaybackAudioFrameBeforeMixing callback and save the audio data in a file
public static class PcmFrameObserver extends FileWriter implements IAudioFrameObserver {
public PcmFrameObserver(String outputFilePath) {
super(outputFilePath);
}
@Override
public int onRecordAudioFrame(AgoraLocalUser agora_local_user, String channel_id, AudioFrame frame) {
System.out.println("onRecordAudioFrame success");
return 1;
}
@Override
public int onPlaybackAudioFrame(AgoraLocalUser agora_local_user, String channel_id, AudioFrame frame) {
System.out.println("onPlaybackAudioFrame success");
return 1;
}
@Override
public int onMixedAudioFrame(AgoraLocalUser agora_local_user, String channel_id, AudioFrame frame) {
System.out.println("onMixedAudioFrame success");
return 1;
}
@Override
public int onPlaybackAudioFrameBeforeMixing(AgoraLocalUser agora_local_user, String channel_id, String uid, AudioFrame audioFrame) {
// Write PCM samples
int writeBytes = audioFrame.getSamplesPerChannel() * audioFrame.getChannels() * 2;
writeData(audioFrame.getBuffer(), writeBytes);
return 1;
}
}
3. 断开连接并释放资源
媒体流接收完成后,你可以通过以下步骤退出频道并断开与声网服务器的连接:
你必须严格按照示例代码中的顺序释放相关资源。
-
调用 unregister 方法释放视频、音频 observer。
Java// Release video and audio observer
localUserObserver.unsetAudioFrameObserver();
localUserObserver.unsetVideoFrameObserver(); -
调用
disconnect
与声网 RTC 频道断开连接。Java// Disconnect from the声网channel
int ret = conn.disconnect();
if (ret != 0) {
System.out.printf("conn.disconnect fail ret=%d\n", ret);
} -
销毁创建的相关对象。
Java// Releases the created objects.
conn.destroy();
注销 AgoraService 对象
当你的服务端程序退出时,可以调用如下逻辑,注销整个 AgoraService
对象。
// Destroy声网 Service
service.destroy();
开发注意事项
- 以上所有接口函数,如无特殊说明,都是异步调用,无需担心阻塞用户线程。
- 在声网 RTC 客户端 SDK 和服务端 SDK 互通的场景中,请确保将客户端 SDK 的频道场景设置为
LIVE_BROADCASTING
。 - SDK 发送 AAC 格式的音频数据时,不支持 44.1 Kbps 的采样率。
参考信息
你可以在这里查看时序图、开发注意事项等信息。
API 参考
你可以通过 API 参考 获取详细的接口和参数信息。
示例项目
声网提供了开源的服务端示例项目供你参考,你可以前往下载或查看其中的源代码。