Android 实现live555 RTSP代理播放器

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/myvest/article/details/51508927

简述

利用live555 实现RTSP拉流客户端,但想看下播放效果,所以结合Android MediaPlayer实现播放。
live555 实现RTSP交互及拉流过程,然后通过UDP,将数据传递给MediaPlayer播放。也就是相当于live555作为RTSP播放器的代理端。这种方案在HTTP/HLS协议的播放器上比较常见。
为什么这么做呢?
好处一个是可以自己实现流媒体交互部分,也可以自己控住媒体数据的缓存。
另一个原因就是比较简单啦。毕竟不用解封装、解码、同步、显示这么多步骤。

后面我还是会将其用mediacodec实现,废话不多说,现在先这样看下测试下流媒体交互部分。目标是播放一路节目,如CCTV风云剧场。
当然,我这么做的前提是使用的Android MediaPlayer本身已经扩展了对UDP播放的支持(其实就是用FFmpeg扩展支持的,但FFmpeg RTSP的支持实在不如live555)。

基本框架:
在这里插入图片描述

RTSP代理端实现

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播放RTSP代理数据

这个如果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,都采用代理的方式实现,因为这样可以很方便的嵌入我们自己的协议交互实现。

最后播放出一路CCTV节目,效果如下:
播放出一路节目
CPU和内存占用也正常。
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/myvest/article/details/51508927