原文地址:Android开发 海康视频 多路视频播放 | Stars-One的杂货小窝
最近公司有个项目需要对接到海康监控摄像头来实现对应的实时播放和回放,但这两个不是我们今天要讨论的重点,APP首页,需要实现同时播放两个视频,全网搜集了下,都没有找到相关资源,于是便是自己研究,最终也是成功实现了功能
注:本文是基于海康视频SDK的demo项目进行功能的增加,默认各位研究阅读了海康SDK文档及已成功运行demo程序的前提下
效果图
首先放下效果图吧
上面的右边即是同时播放了两个视频,两个视频都是一个Fragment,然后各自放在了一个FrameLayout里面
实现
海康设备官方的demo中,是使用了Activity来实现视频播放的功能,但是由于我们这边需要播放多个,页面可以复用一个,只是传的相关参数不同,所以,需要先稍微改造一下官方的那个Activity的demo,改为Fragment
代码比较简单,都是基于官方的demo改了下,相信各位应该可以看懂
布局里只有个SurfaceView,然后需要配置下
Fragment需要在onViewCreated()
方法里设置SurfaceView
的配置选项
Fragment提供了个startVideo()
的方法,需要传递对应的设备参数进来即可实现播放
startVideo()
这里的参数实体类是我自己定义的,各位可以看着改动下,具体是在initVideoSdk()
方法
中进行取值
布局代码:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:background="#021132"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<SurfaceView
android:visibility="gone"
android:id="@+id/svVideo"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
注:源码里代码直接拷贝无法直接使用,需要各位看下然后稍微改动下,使用了EventBus,不需要的可以删除,然后就是视频播放的参数调整应该就没有啥问题了
使用
我这里是使用了两个FrameLayout,将Fragment设置了进去,即首页的右下角,两个FrameLayout是平分了width,我这里是直接使用了ConstraintLayout约束布局加辅助线来实现,代码如下所示
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/videoView"
android:visibility="invisible"
android:orientation="horizontal"
android:layout_width="200dp"
android:layout_height="200dp">
<FrameLayout
android:id="@+id/fl1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/gl"
android:layout_width="0dp"
android:layout_height="match_parent" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/gl"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintGuide_percent="0.5"/>
<FrameLayout
android:id="@+id/fl2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/gl"
android:layout_width="0dp"
android:layout_height="match_parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
使用的话,只需要创建new一个Fragment,之后将其添加到FrameLayout中
//初始化两个fragment
for (int i = 0; i < 2; i++) {
VideoPreviewFragment videoPreviewFragment = new VideoPreviewFragment();
videoPreviewFragments.add(videoPreviewFragment);
if (i == 0) {
FragmentUtils.add(getSupportFragmentManager(), videoPreviewFragment, R.id.fl1, "fragment" + i);
} else {
FragmentUtils.add(getSupportFragmentManager(), videoPreviewFragment, R.id.fl2, "fragment" + i);
}
}
然后再适应的时机,调用startVideo()
,传入对应的参数即可
Fragment的使用说明可以参考这一篇Android开发——Fragment的简单使用总结 - Stars-one - 博客园,这里不再过多赘述
首页的补充说明
首页其实底下是个WebView,然后右下角的是固定悬浮在上面的,由H5那边进行计算,将对应的坐标和长宽传了过来,由APP这边去设置View的宽高
设置View的宽高和大小(是以单位px):
private void setMargins(View v, int l, int t, int width, int height) {
if (v.getLayoutParams() instanceof ViewGroup.MarginLayoutParams) {
ViewGroup.MarginLayoutParams p = (ViewGroup.MarginLayoutParams) v.getLayoutParams();
p.setMargins(l, t, 0, 0);
p.height = height;
p.width = width;
v.requestLayout();
}
}
使用的时候,需要改变View的显示和隐藏,如下代码
//要先隐藏,更改尺寸,再显示,更改尺寸才起作用
videoView.setVisibility(View.GONE);
setMargins(videoView, event.getLeft(), event.getTop(), event.getWidth(), event.getHeight());
videoView.setVisibility(View.VISIBLE);
源码
VideoPreviewFragment源码
package com.tyky.monitorboard.activity;
import android.graphics.PixelFormat;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.ViewGroup;
import com.blankj.utilcode.util.ToastUtils;
import com.hikvision.netsdk.NET_DVR_PREVIEWINFO;
import com.socks.library.KLog;
import com.tyky.monitorboard.R;
import com.tyky.monitorboard.control.DevManageGuider;
import com.tyky.monitorboard.control.SDKGuider;
import com.tyky.monitorboard.event.ShowVideoViewEvent;
import com.tyky.monitorboard.model.BaseVideoChannel;
import com.tyky.monitorboard.utils.HkVideoHelper;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import java.util.ArrayList;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
/**
* 视频实时预览
*/
public class VideoPreviewFragment extends Fragment {
private SurfaceView surfaceView;
private int m_iPreviewHandle = -1; // playback
private int m_iSelectChannel = 1;
//0 main_stream 1 sub_stream 2 third_stream
private int m_iSelectStreamType = 0;
private int m_iUserID = -1; // return by NET_DVR_Login_v30
private BaseVideoChannel baseVideoChannel;
private boolean isDeviceLogin;
public VideoPreviewFragment() {
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
surfaceView = view.findViewById(R.id.svVideo);
configSurface();
}
public void startVideo(BaseVideoChannel baseVideoChannel) {
if (this.baseVideoChannel != null) {
return;
}
this.baseVideoChannel = baseVideoChannel;
initVideoSdk();
//自动开始播放
new Thread(() -> {
try {
//稍微等待5s 视频播放的资源初始化
Thread.sleep(2*1000);
videoPlay();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_video_preview, container, false);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EventBus.getDefault().register(this);
}
private void initVideoSdk() {
//String deviceName = "公司的";
//String ip = "192.9.11.72";
//int port = 8000;
//String userName = "admin";
//String pwd = "tyky_1234";
//m_iSelectChannel = 1;
//m_iSelectStreamType = 1;
String deviceName = baseVideoChannel.getName();
String ip = baseVideoChannel.getIpAddress();
int port = Integer.parseInt(baseVideoChannel.getPort());
String userName = baseVideoChannel.getUserName();
String pwd = baseVideoChannel.getPwd();
m_iSelectChannel = baseVideoChannel.getChannel();
m_iSelectStreamType = Integer.valueOf(baseVideoChannel.getStream());
//设备登录
isDeviceLogin = HkVideoHelper.deviceLogin(deviceName, ip, port, userName, pwd, true);
}
/**
* 初始化surface
*/
private void configSurface() {
surfaceView.getHolder().addCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(@NonNull SurfaceHolder surfaceHolder) {
surfaceView.getHolder().setFormat(PixelFormat.TRANSLUCENT);
if (-1 == m_iPreviewHandle) {
return;
}
Surface surface = surfaceHolder.getSurface();
if (surface.isValid()) {
if (-1 == SDKGuider.g_sdkGuider.m_comPreviewGuider.RealPlaySurfaceChanged_jni(m_iPreviewHandle, 0, surfaceHolder)) {
ToastUtils.showShort("NET_DVR_PlayBackSurfaceChanged" + SDKGuider.g_sdkGuider.GetLastError_jni());
}
}
}
@Override
public void surfaceChanged(@NonNull SurfaceHolder surfaceHolder, int i, int i1, int i2) {
}
@Override
public void surfaceDestroyed(@NonNull SurfaceHolder surfaceHolder) {
if (-1 == m_iPreviewHandle) {
return;
}
if (surfaceHolder.getSurface().isValid()) {
if (-1 == SDKGuider.g_sdkGuider.m_comPreviewGuider.RealPlaySurfaceChanged_jni(m_iPreviewHandle, 0, null)) {
ToastUtils.showShort("NET_DVR_RealPlaySurfaceChanged" + SDKGuider.g_sdkGuider.GetLastError_jni());
}
}
}
});
surfaceView.setZOrderOnTop(true);
}
@Override
public void onDestroy() {
videoStop();
EventBus.getDefault().unregister(this);
super.onDestroy();
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void showVideoView(ShowVideoViewEvent event){
int type = event.getType();
if (type == 1) {
//显示
surfaceView.setVisibility(View.VISIBLE);
}
if (type==0) {
//隐藏
surfaceView.setVisibility(View.INVISIBLE);
}
}
/**
* 开始播放
*/
private void videoPlay() {
if (!isDeviceLogin) {
ToastUtils.showShort("视频设备连接失败,请检查视频设备配置!");
return;
}
KLog.e("--test","视频开始播放");
//当前已连接的设备
ArrayList<DevManageGuider.DeviceItem> devList = SDKGuider.g_sdkGuider.m_comDMGuider.getDevList();
if (devList.size() > 0) {
DevManageGuider.DeviceItem deviceInfo = devList.get(0);
m_iUserID = deviceInfo.m_lUserID;
}
if (m_iPreviewHandle != -1) {
SDKGuider.g_sdkGuider.m_comPreviewGuider.RealPlay_Stop_jni(m_iPreviewHandle);
}
NET_DVR_PREVIEWINFO struPlayInfo = new NET_DVR_PREVIEWINFO();
struPlayInfo.lChannel = m_iSelectChannel;
struPlayInfo.dwStreamType = m_iSelectStreamType;
//bBlocked 0:非阻塞取流 1:阻塞取流
struPlayInfo.bBlocked = 1;
struPlayInfo.hHwnd = surfaceView.getHolder();
m_iPreviewHandle = SDKGuider.g_sdkGuider.m_comPreviewGuider.RealPlay_V40_jni(m_iUserID, struPlayInfo, null);
if (m_iPreviewHandle < 0) {
ToastUtils.showShort("播放失败,原因::" + SDKGuider.g_sdkGuider.GetLastError_jni());
return;
}
ToastUtils.showShort("开始播放");
}
/**
* 停止播放
*/
private void videoStop() {
if (!SDKGuider.g_sdkGuider.m_comPreviewGuider.RealPlay_Stop_jni(m_iPreviewHandle)) {
ToastUtils.showShort("NET_DVR_StopRealPlay m_iPreviewHandle:" + m_iPreviewHandle
+ " error:" + SDKGuider.g_sdkGuider.GetLastError_jni());
return;
}
m_iPreviewHandle = -1;
ToastUtils.showShort("停止播放");
}
}
HkVideoHelper
public class HkVideoHelper {
/**
* 新增视频设备
*
* @param isInsertInDb 是否将数据插入数据库
*/
public static boolean deviceLogin(String devName, String ip, int port, String userName, String pwd, boolean isInsertInDb) {
DevManageGuider.DeviceItem deviceItem = SDKGuider.g_sdkGuider.m_comDMGuider.new DeviceItem();
deviceItem.m_szDevName = devName;
deviceItem.m_struNetInfo = SDKGuider.g_sdkGuider.m_comDMGuider.new DevNetInfo(
ip, port + "", userName, pwd);
if (deviceItem.m_szDevName.isEmpty()) {
deviceItem.m_szDevName = deviceItem.m_struNetInfo.m_szIp;
}
if (SDKGuider.g_sdkGuider.m_comDMGuider.login_v40_jna(deviceItem.m_szDevName, deviceItem.m_struNetInfo)) {
KLog.d("--HkVideoHelper", "设备连接成功");
return true;
} else {
KLog.d("--HkVideoHelper", "失败");
return false;
}
}
}