diff --git a/OnLive/src/main/AndroidManifest.xml b/OnLive/src/main/AndroidManifest.xml
index c420dc5..4b6de5d 100644
--- a/OnLive/src/main/AndroidManifest.xml
+++ b/OnLive/src/main/AndroidManifest.xml
@@ -29,11 +29,12 @@
-
+
diff --git a/OnLive/src/main/java/com/frank/living/activity/MultiScreenActivity.java b/OnLive/src/main/java/com/frank/living/activity/MultiScreenActivity.java
index b42d277..316b2e9 100644
--- a/OnLive/src/main/java/com/frank/living/activity/MultiScreenActivity.java
+++ b/OnLive/src/main/java/com/frank/living/activity/MultiScreenActivity.java
@@ -1,92 +1,425 @@
package com.frank.living.activity;
-import android.os.Bundle;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.PowerManager;
+import android.os.Process;
import android.support.v7.app.AppCompatActivity;
+import android.os.Bundle;
+import android.text.TextUtils;
import android.util.Log;
+import android.view.View;
import com.frank.living.R;
+import com.frank.living.constant.Constants;
+import com.frank.living.listener.OnDoubleClickListener;
import com.frank.living.listener.IjkPlayerListener;
import com.frank.living.widget.IjkVideoView;
+import java.util.HashMap;
+import java.util.TreeMap;
+import tv.danmaku.ijk.media.player.IMediaPlayer;
import tv.danmaku.ijk.media.player.IjkMediaPlayer;
-public class MultiScreenActivity extends AppCompatActivity implements IjkPlayerListener{
+/**
+ * 多路投屏直播
+ * Created by xufulong on 2019/01/04
+ */
+public class MultiScreenActivity extends AppCompatActivity{
- private final static String TAG = MultiScreenActivity.class.getSimpleName();
+ private static final String TAG = MultiScreenActivity.class.getSimpleName();
- private IjkMediaPlayer ijkMediaPlayer;
private IjkVideoView mVideoView1;
private IjkVideoView mVideoView2;
private IjkVideoView mVideoView3;
private IjkVideoView mVideoView4;
- private final static String url1 = "rtsp://184.72.239.149/vod/mp4://BigBuckBunny_175k.mov";
- private final static String url2 = "http://ivi.bupt.edu.cn/hls/cctv5phd.m3u8";
- private final static String url3 = "rtmp://58.200.131.2:1935/livetv/hunantv";
- private final static String url4 = "http://ivi.bupt.edu.cn/hls/cctv1hd.m3u8";
+ private View divider1;
+ private View divider2;
+
+ private IMediaPlayer ijkPlayer;
+ private CustomReceiver customReceiver;
+ private String ipAddress;
+
+ //四分屏模式还是全屏模式
+ private boolean isMultiScreen;
+ //保存客户端ip与通道数对应关系
+ private HashMap clientMap = new HashMap<>();
+ //保存是否投屏与通道数对应关系
+ private TreeMap channelMap = new TreeMap<>();
+
+ private String url = "";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_multi_screen);
- init();
+ parseIntent();
+ registerBroadcast();
- }
+ initView();
+ initListener();
+ setupView();
+ wakeUp();
- private void init(){
- IjkMediaPlayer.loadLibrariesOnce(null);
- IjkMediaPlayer.native_profileBegin("libijkplayer.so");
+ }
+ private void initView(){
mVideoView1 = (IjkVideoView) findViewById(R.id.video_view1);
- mVideoView1.setIjkPlayerListener(this);
- mVideoView1.setVideoPath(url1);
+ mVideoView2 = (IjkVideoView) findViewById(R.id.video_view2);
+ mVideoView3 = (IjkVideoView) findViewById(R.id.video_view3);
+ mVideoView4 = (IjkVideoView) findViewById(R.id.video_view4);
+
+ divider1 = findViewById(R.id.divider1);
+ divider2 = findViewById(R.id.divider2);
+ }
+
+ private void initListener(){
+ mVideoView1.setOnTouchListener(new OnDoubleClickListener(new OnDoubleClickListener.OnDoubleClick() {
+ @Override
+ public void onDouble() {
+ changeScreenMode(1);
+ }
+ }));
+ mVideoView2.setOnTouchListener(new OnDoubleClickListener(new OnDoubleClickListener.OnDoubleClick() {
+ @Override
+ public void onDouble() {
+ changeScreenMode(2);
+ }
+ }));
+ mVideoView3.setOnTouchListener(new OnDoubleClickListener(new OnDoubleClickListener.OnDoubleClick() {
+ @Override
+ public void onDouble() {
+ changeScreenMode(3);
+ }
+ }));
+ mVideoView4.setOnTouchListener(new OnDoubleClickListener(new OnDoubleClickListener.OnDoubleClick() {
+ @Override
+ public void onDouble() {
+ changeScreenMode(4);
+ }
+ }));
+ }
+
+ private void setupView(){
+ //第一路投屏默认全屏
+ enterFullScreen(1);
+ mVideoView1.setVideoPath(url);
+ mVideoView1.setIjkPlayerListener(new IjkPlayerListener() {
+ @Override
+ public void onIjkPlayer(IjkMediaPlayer ijkMediaPlayer) {
+ //设置播放器option
+ setOptions(ijkMediaPlayer);
+ }
+ });
+ mVideoView1.setOnPreparedListener(new IMediaPlayer.OnPreparedListener() {
+ @Override
+ public void onPrepared(IMediaPlayer iMediaPlayer) {
+
+ ijkPlayer = iMediaPlayer;
+ }
+ });
mVideoView1.start();
+ }
- mVideoView2 = (IjkVideoView) findViewById(R.id.video_view2);
- mVideoView2.setVideoPath(url2);
- mVideoView2.start();
+ /**
+ * 外部传进来的数据:URL
+ */
+ private void parseIntent(){
+ for (int i=1; i<=4; i++){
+ channelMap.put(i, false);
+ }
+ clientMap.put(ipAddress, 1);
+ channelMap.put(1, true);
- mVideoView3 = (IjkVideoView) findViewById(R.id.video_view3);
- mVideoView3.setVideoPath(url3);
- mVideoView3.start();
+ Intent intent = getIntent();
+ String url = intent.getStringExtra("url");
+ if(!TextUtils.isEmpty(url)){
+ this.url = url;
+ }
+ String ip = intent.getStringExtra("ip");
+ if (!TextUtils.isEmpty(ip)){
+ ipAddress = ip;
+ }
+ }
- mVideoView4 = (IjkVideoView) findViewById(R.id.video_view4);
- mVideoView4.setVideoPath(url4);
- mVideoView4.start();
+ //唤醒屏幕
+ private void wakeUp(){
+ PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
+ if (powerManager == null)
+ return;
+ PowerManager.WakeLock wakeLock = powerManager.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK
+ |PowerManager.ACQUIRE_CAUSES_WAKEUP, TAG);
+ wakeLock.acquire(1000);
+ wakeLock.release();
}
- private void initOptions(){
- if (ijkMediaPlayer == null)
+ /**
+ * 配置播放器参数项
+ *
+ */
+ private void setOptions(IjkMediaPlayer ijkPlayer){
+ if (ijkPlayer == null)
return;
- Log.e(TAG, "initOptions");
- ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "fast", 1);
- ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "probesize", 200);
- ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "flush_packets", 1);
- ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", 0);
- ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "start-on-prepared", 1);
- //0:代表关闭 1:代表开启
- ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec", 0);
- ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-auto-rotate", 0);
- ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-handle-resolution-change", 0);
- ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "max-buffer-size", 0);
- ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "min-frames", 2);
- ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "max_cached_duration", 30);
- //input buffer:don't limit the input buffer size (useful with realtime streams)
- ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "infbuf", 1);
- ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "fflags", "nobuffer");
- //ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_transport", "tcp");
- ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzedmaxduration", 100);
+ ijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "fast", 1);//不额外优化
+ ijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "probesize", 200);
+ ijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "flush_packets", 1);
+ ijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", 0);//是否开启缓冲
+ ijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "start-on-prepared", 1);
+ //0:代表关闭,1:代表开启
+ ijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec", 0);//开启硬解
+ ijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-auto-rotate", 0);//自动旋屏
+ ijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-handle-resolution-change", 0);//处理分辨率变化
+
+ ijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "max-buffer-size", 0);//最大缓存数
+ ijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "min-frames", 2);//默认最小帧数2
+ ijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "max_cached_duration", 30);//最大缓存时长
+ ijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "infbuf", 1);//是否限制输入缓存数
+ ijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "fflags", "nobuffer");
+ //设置播放前的最大探测时间,分析码流时长:默认1024*1000
+ ijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzedmaxduration", 100);
+ //ijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_transport", "tcp");//tcp传输数据
+ // 音视频同步基线,0-音频为基准,1-视频为基准,3-系统时间戳为基准
+ ijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "av_sync_type", 0);
}
- @Override
- public void onIjkPlayer(IjkMediaPlayer ijkMediaPlayer) {
- this.ijkMediaPlayer = ijkMediaPlayer;
- initOptions();
+ /**
+ * 注册广播
+ */
+ private void registerBroadcast(){
+ customReceiver = new CustomReceiver();
+ IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction(Constants.ACTION_CLIENT_REMOVE);
+ intentFilter.addAction(Constants.ACTION_CLIENT_ADD);
+ registerReceiver(customReceiver, intentFilter);
}
+ /**
+ * 注销广播
+ */
+ private void unregisterBroadcast(){
+ if(customReceiver != null){
+ unregisterReceiver(customReceiver);
+ customReceiver = null;
+ }
+ }
+
+ /**
+ * 自定义广播接收器
+ */
+ private class CustomReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if(action == null)
+ return;
+ Log.e(TAG, "onReceive=" + action);
+ switch (action){
+ case Constants.ACTION_CLIENT_REMOVE: //移除投屏
+ int num = intent.getIntExtra("clientNum", 0);
+ if (num == 0){
+ Process.killProcess(Process.myPid());
+ }else if (num > 0){
+ String ipAddress = intent.getStringExtra("ipAddress");
+ int target = clientMap.get(ipAddress);
+ removeClient(target);
+ clientMap.remove(ipAddress);
+ channelMap.put(target, false);
+ //TODO:多屏变为单屏时,自动切换为全屏
+// if (num == 1){
+// int castingChannel = getCastingChannel();
+// enterFullScreen(castingChannel);
+// }
+ }
+ break;
+ case Constants.ACTION_CLIENT_ADD://新增投屏
+ int clientNum = intent.getIntExtra("clientNum", 0);
+ String otherUrl = intent.getStringExtra("url");
+ String ipAddress = intent.getStringExtra("ip");
+ //选择空闲通道
+ int channel = selectIdleChannel(clientNum);
+ clientMap.put(ipAddress, channel);
+ channelMap.put(channel, true);
+ addClient(channel, otherUrl);
+ //TODO:单屏变为两路投屏时,自动切换为多屏模式
+ if (clientNum == 2){
+ exitFullScreen();
+ }
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ /**
+ * 选择空闲通道
+ * @param clientNum clientNum
+ * @return idleChannel
+ */
+ private int selectIdleChannel(int clientNum){
+ for (int channel = 1; channel < clientNum; channel++){
+ if (!channelMap.get(channel)){
+ return channel;
+ }
+ }
+ return clientNum;
+ }
+
+ /**
+ * 获取当前投屏通道
+ * @return idleChannel
+ */
+ private int getCastingChannel(){
+ for (int channel = 1; channel <= 4; channel++){
+ if (channelMap.get(channel)){
+ return channel;
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * 添加客户端
+ * @param target target
+ * @param clientUrl clientUrl
+ */
+ private void addClient(int target, String clientUrl){
+ switch (target){
+ case 1:
+ mVideoView1.setVisibility(View.VISIBLE);
+ mVideoView1.setVideoPath(clientUrl);
+ mVideoView1.start();
+ break;
+ case 2:
+ mVideoView2.setVisibility(View.VISIBLE);
+ mVideoView2.setVideoPath(clientUrl);
+ mVideoView2.start();
+ break;
+ case 3:
+ mVideoView3.setVisibility(View.VISIBLE);
+ mVideoView3.setVideoPath(clientUrl);
+ mVideoView3.start();
+ break;
+ case 4:
+ mVideoView4.setVisibility(View.VISIBLE);
+ mVideoView4.setVideoPath(clientUrl);
+ mVideoView4.start();
+ break;
+ default:
+ break;
+ }
+ }
+
+ /**
+ * 移除客户端
+ * @param target target
+ */
+ private void removeClient(int target){
+ switch (target){
+ case 1:
+ mVideoView1.stopPlayback();
+ mVideoView1.setVisibility(View.GONE);
+ break;
+ case 2:
+ mVideoView2.stopPlayback();
+ mVideoView2.setVisibility(View.GONE);
+ break;
+ case 3:
+ mVideoView3.stopPlayback();
+ mVideoView3.setVisibility(View.GONE);
+ break;
+ case 4:
+ mVideoView4.stopPlayback();
+ mVideoView4.setVisibility(View.GONE);
+ break;
+ default:
+ break;
+ }
+ }
+
+ /**
+ * 隐藏分割线
+ */
+ private void hideDivider(){
+ divider1.setVisibility(View.GONE);
+ divider2.setVisibility(View.GONE);
+ }
+
+ /**
+ * 显示分割线
+ */
+ private void showDivider(){
+ divider1.setVisibility(View.VISIBLE);
+ divider2.setVisibility(View.VISIBLE);
+ }
+
+ /**
+ * 进入全屏模式
+ * @param channel channel
+ */
+ private void enterFullScreen(int channel){
+ hideDivider();
+ switch (channel){
+ case 1:
+ mVideoView1.setVisibility(View.VISIBLE);
+ mVideoView2.setVisibility(View.GONE);
+ mVideoView3.setVisibility(View.GONE);
+ mVideoView4.setVisibility(View.GONE);
+ break;
+ case 2:
+ mVideoView1.setVisibility(View.GONE);
+ mVideoView2.setVisibility(View.VISIBLE);
+ mVideoView3.setVisibility(View.GONE);
+ mVideoView4.setVisibility(View.GONE);
+ break;
+ case 3:
+ mVideoView1.setVisibility(View.GONE);
+ mVideoView2.setVisibility(View.GONE);
+ mVideoView3.setVisibility(View.VISIBLE);
+ mVideoView4.setVisibility(View.GONE);
+ break;
+ case 4:
+ mVideoView1.setVisibility(View.GONE);
+ mVideoView2.setVisibility(View.GONE);
+ mVideoView3.setVisibility(View.GONE);
+ mVideoView4.setVisibility(View.VISIBLE);
+ break;
+ default:
+ break;
+ }
+ }
+
+ /**
+ * 退出全屏模式
+ */
+ private void exitFullScreen(){
+ showDivider();
+ mVideoView1.setVisibility(View.VISIBLE);
+ mVideoView2.setVisibility(View.VISIBLE);
+ mVideoView3.setVisibility(View.VISIBLE);
+ mVideoView4.setVisibility(View.VISIBLE);
+ }
+
+ /**
+ * 切换分屏模式
+ * @param channel channel
+ */
+ private void changeScreenMode(int channel){
+ isMultiScreen = !isMultiScreen;
+ if (isMultiScreen){
+ enterFullScreen(channel);
+ }else {
+ exitFullScreen();
+ }
+ }
@Override
- protected void onStop() {
- super.onStop();
+ protected void onDestroy() {
+ super.onDestroy();
+
+ unregisterBroadcast();
mVideoView1.stopPlayback();
mVideoView1.release(true);
@@ -101,3 +434,4 @@ public class MultiScreenActivity extends AppCompatActivity implements IjkPlayerL
}
}
+
diff --git a/OnLive/src/main/java/com/frank/living/constant/Constants.java b/OnLive/src/main/java/com/frank/living/constant/Constants.java
new file mode 100644
index 0000000..4369c7f
--- /dev/null
+++ b/OnLive/src/main/java/com/frank/living/constant/Constants.java
@@ -0,0 +1,10 @@
+package com.frank.living.constant;
+
+public class Constants {
+
+ //新增投屏广播
+ public final static String ACTION_CLIENT_ADD = "action.client.ADD";
+ //移除投屏广播
+ public final static String ACTION_CLIENT_REMOVE = "action.client.REMOVE";
+
+}
diff --git a/OnLive/src/main/java/com/frank/living/listener/OnDoubleClickListener.java b/OnLive/src/main/java/com/frank/living/listener/OnDoubleClickListener.java
new file mode 100644
index 0000000..60b7850
--- /dev/null
+++ b/OnLive/src/main/java/com/frank/living/listener/OnDoubleClickListener.java
@@ -0,0 +1,59 @@
+package com.frank.living.listener;
+
+import android.view.MotionEvent;
+import android.view.View;
+
+/**
+ * 双击事件监听
+ * Created by xufulong on 2019/2/27.
+ */
+
+public class OnDoubleClickListener implements View.OnTouchListener {
+
+ private final static long DOUBLE_TIME = 500;
+ private long firstClick;
+ private long secondClick;
+ private int clickCount;
+
+ private OnDoubleClick onDoubleClick;
+
+ public OnDoubleClickListener(OnDoubleClick onDoubleClick){
+ this.onDoubleClick = onDoubleClick;
+ }
+
+ public interface OnDoubleClick{
+ void onDouble();
+ }
+
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ if (event.getAction() == MotionEvent.ACTION_UP){
+ return v.performClick();
+ }else if (event.getAction() == MotionEvent.ACTION_DOWN){
+ clickCount ++;
+ if (clickCount == 1){
+ firstClick = System.currentTimeMillis();
+ }else if (clickCount == 2){
+ secondClick = System.currentTimeMillis();
+ if (secondClick - firstClick <= DOUBLE_TIME){
+ firstClick = 0;
+ clickCount = 1;
+ if (onDoubleClick != null){
+ onDoubleClick.onDouble();
+ }
+ }else {
+ firstClick = secondClick;
+ clickCount = 1;
+ }
+ secondClick = 0;
+ }else {
+ clickCount = 0;
+ firstClick = 0;
+ secondClick = 0;
+ }
+ }
+
+ return false;
+ }
+
+}
diff --git a/OnLive/src/main/java/com/frank/living/widget/IjkVideoView.java b/OnLive/src/main/java/com/frank/living/widget/IjkVideoView.java
index 8e81e7f..f451c7a 100644
--- a/OnLive/src/main/java/com/frank/living/widget/IjkVideoView.java
+++ b/OnLive/src/main/java/com/frank/living/widget/IjkVideoView.java
@@ -1182,23 +1182,9 @@ public class IjkVideoView extends FrameLayout implements MediaController.MediaPl
return language;
}
- public ITrackInfo[] getTrackInfo() {
- if (mMediaPlayer == null)
- return null;
-
- return mMediaPlayer.getTrackInfo();
- }
-
- public void selectTrack(int stream) {
- MediaPlayerCompat.selectTrack(mMediaPlayer, stream);
- }
-
- public void deselectTrack(int stream) {
- MediaPlayerCompat.deselectTrack(mMediaPlayer, stream);
- }
-
- public int getSelectedTrack(int trackType) {
- return MediaPlayerCompat.getSelectedTrack(mMediaPlayer, trackType);
+ @Override
+ public boolean performClick() {
+ return super.performClick();
}
}
diff --git a/README.md b/README.md
index 0d38d19..4045db5 100644
--- a/README.md
+++ b/README.md
@@ -30,6 +30,8 @@ android端基于FFmpeg库的使用。
- #### OpenGL+GPUImage滤镜
- #### FFmpeg的AVFilter滤镜
+- #### IjkPlayer的RTSP超低延时直播
+- #### IjkPlayer的RTSP多路投屏直播
左边是ffplay客户端拉流播放,中间是web网页播放: