利用live555 实现RTSP拉流客户端,但想看下播放效果,所以结合Android MediaPlayer实现播放。
live555 实现RTSP交互及拉流过程,然后通过UDP,将数据传递给MediaPlayer播放。也就是相当于live555作为RTSP播放器的代理端。这种方案在HTTP/HLS协议的播放器上比较常见。
为什么这么做呢?
好处一个是可以自己实现流媒体交互部分,也可以自己控住媒体数据的缓存。
另一个原因就是比较简单啦。毕竟不用解封装、解码、同步、显示这么多步骤。
后面我还是会将其用mediacodec实现,废话不多说,现在先这样看下测试下流媒体交互部分。目标是播放一路节目,如CCTV风云剧场。
当然,我这么做的前提是使用的Android MediaPlayer本身已经扩展了对UDP播放的支持(其实就是用FFmpeg扩展支持的,但FFmpeg RTSP的支持实在不如live555)。
RTSP代理端实现核心基于live555的测试用例代码testRTSPClient.cpp。
1、JAVA层提供两个接口
package com.example.testplayer;
public class RTSPSource {
static {
System.loadLibrary("RTSTSource_jni");
}
public native int start(String url);
public native void stop();
}
2、JNI实现
static jint jni_start(JNIEnv* env, jobject obj, jstring filename){
int ret = -1;
const char* url = env->GetStringUTFChars(filename, NULL);
ret = start(url);
env->ReleaseStringUTFChars(filename, url);
return (jint)ret;
}
static void jni_stop(JNIEnv* env, jobject obj){
stop();
return;
}
可以看出没太多东西,主要还是在live555用例修改
3、RTSP交互
基于testRTSPClient.cpp修改:
1)live555 中,所有事件,包括socket的读写事件,延迟事件,触发事件,都在doEventLoop的循环中处理。所以需要将其放在另一个线程中执行,结果通过success变量来获取。主线程中等待1.5秒内RTSP交互完成。
如果流程已经走到continueAfterPLAY中,即说明RTSP交互已经完成,在其中将条件变量返回。
Boolean success = False;
pthread_t gRtspLoopID;
pthread_mutex_t successMutex;
pthread_cond_t successCond;
static void* rtspLoop(void* argv){
// Begin by setting up our usage environment:
TaskScheduler* scheduler = BasicTaskScheduler::createNew();
UsageEnvironment* env = BasicUsageEnvironment::createNew(*scheduler);
openURL(*env, "rtspsource", (char* )argv);
LOGE("--------------------All subsequent activity takes place within the event loop\n");
eventLoopWatchVariable = 0;
env->taskScheduler().doEventLoop(&eventLoopWatchVariable);
return NULL;
}
int start(const char* url) {
if (url == NULL) {
return -1;
}
pthread_mutex_init(&successMutex, NULL);
pthread_cond_init(&successCond, NULL);
pthread_create(&gRtspLoopID, NULL,rtspLoop,(void*)url);
pthread_mutex_lock(&successMutex);
pthread_cond_timeout_np(&successCond, &successMutex, 1500);//1.5s timeout
pthread_mutex_unlock(&successMutex);
if(success){
LOGE("--------------------RTSP START SUCCESS!!!!");
return 0;
}
LOGE("--------------------RTSP START FAIL!!!!");
return -1;
}
void stop() {
LOGE("--------------------RTSP STOP!!!!");
eventLoopWatchVariable = 1;
success = False;
pthread_join(gRtspLoopID,NULL);
pthread_mutex_destroy(&successMutex);
pthread_cond_destroy(&successCond);
}
void continueAfterPLAY(RTSPClient* rtspClient, int resultCode, char* resultString) {
success = False;
......省略
LOGE("Started playing session\n") ;
success = True;
pthread_mutex_lock(&successMutex);
pthread_cond_signal(&successCond);
pthread_mutex_unlock(&successMutex);
} while (0);
......省略
2)DummySink修改,之前在其他文章中说过Sink 接收端, 流的终点, 可理解为是消费者。
每次收取一帧数据,会调用到afterGettingFrame,通过continuePlaying又会处理获取下一帧数据,从而成为一个循环。这两个函数需要自己实现,测试用例的DummySink已经实现好了,但只是将流信息打印出来,在此可以将流媒体数据发送给MediaPlayer,socket可以直接用live555封装好的接口,数据发给127.0.0.1:14321。也可以在此dump流。
这里 writeSocket函数的portNumBits portNum 参数,需要将端口调整为网络字节序,我一直以为是直接传端口号。因为没注意这个细节,当时调了一两个小时也没能播放,通过busybox netstat -apn 命令可以看到端口确实已经正确绑定。之后通过 tcpdump -i lo 命令,打印出回环地址的数据交互,才发现问题。。。
细节真是太重要了。
第二个修改是需要增加心跳发送,这个RTSP协议应该是有这个要求的,但live555的这个测试用例代码没实现,这样播放一会,CDN可能会将流断开(我测试的时候会这样)。心跳通过option和GetParameter都是可以的,这里是一分钟发一次。
void DummySink::afterGettingFrame(unsigned frameSize, unsigned numTruncatedBytes,
struct timeval presentationTime, unsigned /*durationInMicroseconds*/) {
#ifdef DEBUG_DUMP_TS
if(mDumpFile) fwrite(fReceiveBuffer, frameSize, 1, mDumpFile);
#endif
#ifdef SOCKET_SEND
if(mOutsock < 0){
const Port port(11234);
mOutsock = setupDatagramSocket(envir(), port);
if(mOutsock < 0)
{
envir() << "socket error\n";
}
}
struct in_addr sessionAddress;
sessionAddress.s_addr = our_inet_addr("127.0.0.1");
portNumBits portNum = htons(14321);
writeSocket(envir(), mOutsock, sessionAddress, portNum, (unsigned char *)fReceiveBuffer, frameSize);
#endif
gettimeofday(&presentationTime, NULL);
if((int64_t)presentationTime.tv_sec - lastHartbitTime > 60){
RTSPClient* rtspClient = (RTSPClient*)(fSubsession.miscPtr);
StreamClientState& scs = ((ourRTSPClient*)rtspClient)->scs; // alias
rtspClient->sendGetParameterCommand(*scs.session, NULL,"");
LOGE("sendGetParameterCommand\n");
lastHartbitTime = (int64_t)presentationTime.tv_sec;
}
// Then continue, to request the next frame of data:
continuePlaying();
}
3)第三个修改是我用来测试播放的这路节目RTSP的交互协议和标准协议有些扩展,这个就需要在RTSPClient里修改了,具体修改就不贴出来了。一般的RTSP live555不用修改就能支持。
这个如果MediaPlayer已经支持UDP播放,那就没什么了,像平常调用即可。
上面RTSP代理是将数据发给127.0.0.1:14321,如果是基于FFmpeg扩展的,那么在播放地址带上localport参数即可绑定端口。
看下FFmpeg UDP协议的open函数。
static int udp_open(URLContext *h, const char *uri, int flags)
......省略
if (av_find_info_tag(buf, sizeof(buf), "udplite_coverage", p)) {
s->udplite_coverage = strtol(buf, NULL, 10);
}
if (av_find_info_tag(buf, sizeof(buf), "localport", p)) {
s->local_port = strtol(buf, NULL, 10);
}
MediaPlayer播放简单示例如下:
public class ProxyPlayer implements OnPreparedListener{
......省略
private MediaPlayer mPlayer;
private RTSPSource mRTSP;
public void setDataSource(String url){
Log.e(TAG,"setDataSource " + url);
mRTSPUrl = url;
try {
mPlayer.setDataSource("udp://127.0.0.1:11234?localport=14321");
} catch (Exception e) {
e.printStackTrace();
}
}
public void start(){
int ret = mRTSP.start(mRTSPUrl);
if(ret != 0){
Log.e(TAG,"RTSP Start ERROR!!!");
return;
}
mPlayer.prepareAsync();
}
......省略
}
总体来说比较简单,主要在于live555的RTSP交互扩展支持及测试用例代码testRTSPClient.cpp测试用例代码的修改,还要就是一些JNI的实现工作。MediaPlayer部分比较简单,当然这前提是MediaPlayer对UDP的扩展支持之前已经实现了。
这个做法可以用来帮助我们调试流媒体协议,包括RTSP/HLS都可以这么干。其实很多HLS播放器APK,都采用代理的方式实现,因为这样可以很方便的嵌入我们自己的协议交互实现。