开发文档:OPEN API/云直播/播放地址/播放地址接口(新)/获取播放地址
获取播放地址
接口功能:
该接口用于通过设备序列号、通道号获取单台设备的播放地址信息,无法获取永久有效期播放地址。
请求地址
https://open.ys7.com/api/lapp/v2/live/address/get
子账户token请求所需最小权限
"Permission":"Get" "Resource":"dev:序列号"
请求方式
POST
请求参数
参数名 类型 描述 是否必选
accessToken String 授权过程获取的access_token Y
deviceSerial String 设备序列号例如427734222,均采用英文符号,限制最多50个字符 Y
channelNo Integer 通道号,非必选,默认为1 N
protocol Integer 流播放协议,1-ezopen、2-hls、3-rtmp、4-flv,默认为1 N
code String ezopen协议地址的设备的视频加密密码 N
expireTime Integer 过期时长,单位秒;针对hls/rtmp/flv设置有效期,相对时间;30秒-720天 N
type String 地址的类型,1-预览,2-本地录像回放,3-云存储录像回放,非必选,默认为1;回放仅支持rtmp、ezopen、flv协议 N
quality Integer 视频清晰度,1-高清(主码流)、2-流畅(子码流) N
startTime String 本地录像/云存储录像回放开始时间,云存储开始结束时间必须在同一天,示例:2019-12-01 00:00:00 N
stopTime String 本地录像/云存储录像回放结束时间,云存储开始结束时间必须在同一天,示例:2019-12-01 23:59:59 N
supportH265 Integer 请判断播放端是否要求播放视频为H265编码格式,1表示需要,0表示不要求 N
playbackSpeed String 回放倍速。倍速为 -1( 支持的最大倍速)、0.5、1、2、4、8、16;
仅支持protocol为4-flv
且
type为2-本地录像回放( 部分设备可能不支持16倍速) 或者 3-云存储录像回放 N
gbchannel String 国标设备的通道编号,视频通道编号ID N
HTTP请求报文
POST /api/lapp/v2/live/address/get HTTP/1.1
Host: open.ys7.com
Content-Type: application/x-www-form-urlencoded
accessToken=at.dunwhxt2azk02hcn7phqygsybbw0wv6p&deviceSerial=C78957921&channelNo=1
返回数据
{
"msg": "Operation succeeded",
"code": "200",
"data": {
"id": "254708522214232064",
"url": "https://open.ys7.com/v3/openlive/C78957921_1_1.m3u8?expire=1606999273&id=254708522214232064&t=093e5c6668d981e0f0b8d2593d69bdc98060407d1b2f42eaaa17a62b15ee4f99&ev=100",
"expireTime": "2020-12-03 20:41:13"
}
} OPEN API/云直播/录像文件查询/云存储录像查询/根据时间获取存储文件信息 根据时间获取存储文件信息
接口功能:
该接口用于根据时间获取存储文件信息。注:
请求地址:
https://open.ys7.com/api/lapp/video/by/time
请求方式
POST
子账户token请求所需最小权限
无
请求参数;
参数名 类型 描述 是否必选
accessToken String 访问令牌 Y
deviceSerial String 设备序列号,存在英文字母的设备序列号,字母需为大写 Y
channelNo int 通道号,非必选,默认为1 N
startTime long 起始时间,时间格式为:1378345128000。非必选,默认为当天0点 N
endTime long 结束时间,时间格式为:1378345128000。非必选,默认为当前时间 N
recType int 回放源,0-系统自动选择,1-云存储,2-本地录像。非必选,默认为0 N
version String 返回分页结构,recType=1时,传2.0会返回分页结构; recType=2时,传2.0且pageSize不为空的情况才会返回分页结构 N
pageSize int recType为1或2时,可指定返回的文件数量,云存储类型分页大小范围:1-1000,本地录像类型分页大小范围: 1-500 N
HTTP请求报文
POST /api/lapp/alarm/video HTTP/1.1
Host: open.ys7.com
Content-Type: application/x-www-form-urlencoded
accessToken=at.dunwhxt2azk02hcn7phqygsybbw0wv6p&deviceSerial=427734203&channelNo=1&startTime=1378345128000&endTime=1378345128000&recType=0
返回数据
{
“code”: “200”,
“msg”: “操作成功”,
“data”: [
{
“recType”: 0,
“startTime”: 1378345128000,
“endTime”: 1378345128000,
“deviceSerial”: “409864662”,
“cameraNo”: “1”,
“localType”: “ALLEVENT”,
“channelType”: “D”,
“id”: 20432171600,
“fileId”: “20432171600”,
“ownerId”: “chenyong”,
“fileType”: 1,
“fileName”: “”,
“cloudType”: 1,
“fileIndex”: “5d5b6d94-13e8-440b-a25b-00eda521c35f”,
“fileSize”: 4011828,
“locked”: 0,
“createTime”: “2016-08-22 13:59:13”,
“crypt”: 22,
“keyChecksum”: “”,
“videoLong”: 150000,
“coverPic”: “https://218.244.139.5:0/api/cloud?method=download&fid=a14f8348-1dd1-11b2-aef9-dbed68cc4c3e&session=hik%24shipin7%231%23USK%23at.a2rwv07y6v44ozhqblhb0tly337hb4vq-4jktv8rbjh-193fe5b-cm38stbht”,
“downloadPath”: “218.244.139.5:0”,
“type”: 1
},
{
“recType”: 0,
“startTime”: 1378345128000,
“endTime”: 1378345128000,
“deviceSerial”: “409864662”,
“cameraNo”: “1”,
“localType”: “ALLEVENT”,
“channelType”: “D”,
“id”: 20432171600,
“fileId”: “20432171600”,
“ownerId”: “chenyong”,
“fileType”: 1,
“fileName”: “”,
“cloudType”: 1,
“fileIndex”: “5d5b6d94-13e8-440b-a25b-00eda521c35f”,
“fileSize”: 4011828,
“locked”: 0,
“createTime”: “2016-08-22 13:59:13”,
“crypt”: 22,
“keyChecksum”: “”,
“videoLong”: 150000,
“coverPic”: “https://218.244.139.5:0/api/cloud?method=download&fid=a14f8348-1dd1-11b2-aef9-dbed68cc4c3e&session=hik%24shipin7%231%23USK%23at.a2rwv07y6v44ozhqblhb0tly337hb4vq-4jktv8rbjh-193fe5b-cm38stbht”,
“downloadPath”: “218.244.139.5:0”,
“type”: 1
}
]
}
}
返回数据 (分页结构返回)
{
“msg”: “操作成功!”,
“code”: “200”,
“data”: {
“files”: [
{
“recType”: 2,
“startTime”: 1691627391000,
“endTime”: 1691627443000,
“deviceSerial”: “G12262381”,
“channelNo”: 1,
“localType”: “ALARM”,
“channelType”: “D”,
“id”: null,
“fileId”: null,
“ownerId”: null,
“fileType”: 0,
“fileName”: null,
“cloudType”: 0,
“fileIndex”: null,
“fileSize”: 0,
“locked”: 0,
“createTime”: 0,
“crypt”: 0,
“keyChecksum”: null,
“videoLong”: 0,
“coverPic”: null,
“downloadPath”: null,
“type”: 1,
“iStorageVersion”: null,
“videoType”: null
},
{
“recType”: 2,
“startTime”: 1691627491000,
“endTime”: 1691627537000,
“deviceSerial”: “G12262381”,
“channelNo”: 1,
“localType”: “ALARM”,
“channelType”: “D”,
“id”: null,
“fileId”: null,
“ownerId”: null,
“fileType”: 0,
“fileName”: null,
“cloudType”: 0,
“fileIndex”: null,
“fileSize”: 0,
“locked”: 0,
“createTime”: 0,
“crypt”: 0,
“keyChecksum”: null,
“videoLong”: 0,
“coverPic”: null,
“downloadPath”: null,
“type”: 1,
“iStorageVersion”: null,
“videoType”: null
},
{
“recType”: 2,
“startTime”: 1691627537000,
“endTime”: 1691627586000,
“deviceSerial”: “G12262381”,
“channelNo”: 1,
“localType”: “ALARM”,
“channelType”: “D”,
“id”: null,
“fileId”: null,
“ownerId”: null,
“fileType”: 0,
“fileName”: null,
“cloudType”: 0,
“fileIndex”: null,
“fileSize”: 0,
“locked”: 0,
“createTime”: 0,
“crypt”: 0,
“keyChecksum”: null,
“videoLong”: 0,
“coverPic”: null,
“downloadPath”: null,
“type”: 1,
“iStorageVersion”: null,
“videoType”: null
}
],
“isAll”: false,
“nextFileTime”: 1691627586000
}
} OPEN API/云直播/录像文件查询/设备本地录像查询/查询设备本地录像 查询设备本地录像
接口URL
GET https://open.ys7.com/api/v3/device/local/video/unify/query
请求
Header
名称 类型 必填 描述 示例指及参考API
accessToken string Y 萤石开放平台令牌,支持托管 、子账号、设备小权限token,权限为Replay
deviceSerial string Y 设备序列号
localIndex string N 通道号
query
名称 类型 必填 描述 示例指及参考API
recordType int N 1:定时录像 2:事件录像 3:智能-车 4:智能-人形 5:自动浓缩录像,不填默认查询所有类型
startTime string Y 开始时间,时间格式为:1731988238,开始结束时间必须在同一天,开始时间不能大于结束时间
endTime string Y 结束时间,时间格式为:1732007943,开始结束时间必须在同一天,开始时间不能大于结束时间
isQueryByNvr int N 是否反查NVR录像:0-不反查(默认),1-反查NVR
location int N 录像检索位置:1-本地录像检索(默认),2-CVR中心录像检索
pageSize int N 分页的页面大小,默认50,最大200
响应
返回数据
名称 类型 描述 示例
meta object meta
-code int code
-message string message
-moreInfo object moreInfo
data object data
-records arrayrecords
–startTime int startTime
–endTime int endTime
–type string type
–size string size
-fromNvr boolean fromNvr
-deviceSerial string deviceSerial
-localIndex string localIndex
-hasMore boolean hasMore
-nextFileTime int nextFileTime
返回示例
{
“meta”: {
“code”: 200,
“message”: “操作成功”,
“moreInfo”: null
},
“data”: {
“records”: [
{
“startTime”: 1731945592,
“endTime”: 1731949200,
“type”: “ALARM”, //录像类型
“size”: “” //录像文件大小,单位:字节
}
],
“fromNvr”: true, //该录像文件是否来自关联的nvr
“deviceSerial”: “J79401957”, // fromNvr为true,则返回关联NVR设备序列号,否则返回入参填的设备序列号。
“localIndex”: “1”, // fromNvr为true,则返回关联NVR设备通道号,否则返回入参填的设备通道号。
“hasMore”: true, //是否存在更多录像文件
“nextFileTime”: 1732007943// hasMore为true时,该参数值为下一个录像文件的开始时间。如需分页查询,该参数值可作为下一页录像文件查询的开始时间。
}
} SDK及示例/Android SDK/Android 回放/回放
回放
对摄像机存储于SD卡、云端的录像进行取流,查看当前摄像机的历史回放画面。
第一步创建播放器
可调用EZOpenSDK中的 createPlayer 方法创建播放器。
第二步配置播放器
播放器创建完成后需要进行设置代理,设置播放视图,验证码设置等配置。
第三步开始播放
调用startPlayback(EZCloudRecordFile cloudFile) 或 startPlayback(EZDeviceRecordFile deviceFile) 开始回放
第四步结束播放
调用stopPlayback结束回放
5.第五步释放播放器
调用release释放播放器
完整示例代码如下:
public class EZPlayBackListActivity extends RootActivity implements TextureView.SurfaceTextureListener, Handler.Callback, … {
private EZPlayer mPlaybackPlayer = null;
/**
点击录像片段后调用
*/
private void initEZPlayer() {
if (mPlaybackPlayer != null) {
// do something
// 停止播放
mPlaybackPlayer.stopPlayback();
} else {
// 创建播放器,也可以直接使用EZPlayer类中的方法创建
mPlaybackPlayer = getOpenSDK().createPlayer(mCameraInfo.getDeviceSerial(), mCameraInfo.getCameraNo());
// 设置Handler, 该handler将被用于从播放器向handler传递消息
mPlaybackPlayer.setHandler(playBackHandler);
// 设置播放器的显示Surface
mPlaybackPlayer.setSurfaceEx(mTextureView.getSurfaceTexture());
// 可选,设备开启了视频/图片加密功能后需设置,可根据EZDeviceInfo的isEncrypt属性判断
mPlaybackPlayer.setPlayVerifyCode(verifyCode);
// 回放云端存储的视频,cloudFile由EZOpenSDK.searchRecordFileFromCloud接口获取
mPlaybackPlayer.startPlayback(cloudFile);
// 或者
// 回放设备上存储的视频,deviceFile由EZOpenSDK.searchRecordFileFromDevice接口获取
mPlaybackPlayer.startPlayback(deviceFile);
}
}
@Override
protected void onStop() {
super.onStop();
if (mPlaybackPlayer != null) {
// 页面退出或用户主动停止播放时调用stopPlayback结束回放
mPlaybackPlayer.stopPlayback();
}
// do something
}
@Override protected void onDestroy() { super.onDestroy(); if (mPlaybackPlayer != null) { // 调用release释放播放器 mPlaybackPlayer.release(); } // do something }
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) {
// 画面显示第一帧
case EZPlaybackConstants.MSG_REMOTEPLAYBACK_PLAY_SUCCUSS:// 录像回放成功
// do something
break;
case EZPlaybackConstants.MSG_REMOTEPLAYBACK_PLAY_START:// 播放开始|seek成功
// do something
break;
case EZPlaybackConstants.MSG_REMOTEPLAYBACK_STOP_SUCCESS:// 录像回放停止
// do something
break;
case EZPlaybackConstants.MSG_REMOTEPLAYBACK_PLAY_FINISH:// 录像回放完成
// do something
break;
case EZPlaybackConstants.MSG_REMOTEPLAYBACK_PLAY_FAIL:// 录像回放失败
ErrorInfo errorInfo = (ErrorInfo) msg.obj;
int errorCode = errorInfo.errorCode;
// 如果是需要验证码或者是验证码错误
if (errorCode == ErrorCode.ERROR_INNER_VERIFYCODE_NEED || errorCode == ErrorCode.ERROR_INNER_VERIFYCODE_ERROR) {
// do something
} else {
// do something
}
break;
default:
break;
}
}
}
说明
回放需先获取到视频信息,searchRecordFileFromCloud方法和searchRecordFileFromDevice方法分别是获取云端视频列表和设备存储视频列表的两个方法。
开始播放之后在handleMessage回调中会收到通知消息,播放成功消息为EZPlaybackConstants.MSG_REMOTEPLAYBACK_PLAY_SUCCUSS, 播放失败消息为EZPlaybackConstants.MSG_REMOTEPLAYBACK_PLAY_FAIL,如果是错误码ErrorCode.ERROR_INNER_VERIFYCODE_NEED = 400035(需要设备验证码)或者ErrorCode.ERROR_INNER_VERIFYCODE_ERROR = 400036(设备验证码不匹配),需要开发者自己处理让用户输入验证密码,然后调用EZPlayer.setPlayVerifyCode设置密码,重新启动播放。
注意:实际录像回放结束时间可能与录像片段的结束时间有偏差,如果时间点相近则认为回放结束,此为正常现象。严格依据上述开发文档检查并解决从MainActivity.java点击id.huifang后进入FanHui.java整个程序卡死无任何响应。MainActivity.java:package com.videogo.ui.login;
import android.content.res.Configuration;
import android.content.pm.ActivityInfo;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import com.videogo.exception.BaseException;
import com.videogo.exception.ErrorCode;
import com.videogo.openapi.EZConstants;
import com.videogo.openapi.EZOpenSDK;
import com.videogo.openapi.EZPlayer;
import ezviz.ezopensdk.R;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.LinearLayout;
import android.widget.ImageButton;
import android.widget.Toast;
import android.view.MotionEvent;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URLEncoder;
import org.json.JSONObject;
import android.content.Intent;
import android.widget.Button;
// 新增导入
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class MainActivity extends AppCompatActivity implements SurfaceHolder.Callback, Handler.Callback {
private static final String TAG = “EZPreview”;
private static final int MSG_VIDEO_SIZE_CHANGED = 1;
private static final int MSG_REALPLAY_PLAY_SUCCESS = 2001;
private static final int MSG_REALPLAY_PLAY_FAIL = 2002;
private static final int MSG_SHOW_STREAM_TYPE = 3001; // 新增消息类型
private static final int DIRECTION_UP = 0;
private static final int DIRECTION_DOWN = 1;
private static final int DIRECTION_LEFT = 2;
private static final int DIRECTION_RIGHT = 3;
private static final int DIRECTION_ZOOM_IN = 8; // 物理放大
private static final int DIRECTION_ZOOM_OUT = 9; // 物理缩小
private static final int DIRECTION_FOCUS_NEAR = 10; // 调整近焦距
private static final int DIRECTION_FOCUS_FAR = 11; // 调整远焦距
private static final int SPEED_MEDIUM = 1; // 速度适中
private static final String PTZ_START_URL = “https://open.ys7.com/api/lapp/device/ptz/start”;
private static final String PTZ_STOP_URL = “https://open.ys7.com/api/lapp/device/ptz/stop”;
private boolean mIsSupportPTZ = false; // 设备是否支持云台
private ExecutorService mExecutorService; // 线程池
private volatile boolean isPTZActive = false; // 标记是否有云台操作正在进行
private volatile int activeDirection = -1; // 当前活动的方向
private boolean mHasShownConnectionSuccess = false;
// 接收的参数键
private static final String KEY_APPKEY = “appkey”;
private static final String KEY_SERIAL = “serial”;
private static final String KEY_VERIFYCODE = “VerifyCode”;
private static final String KEY_ACCESSTOKEN = “accessToken”;
private static final String KEY_CAMERANO = “cameraNo”;
private boolean mIsPlaying = false;
private EZPlayer mEZPlayer;
private SurfaceView mSurfaceView;
private SurfaceHolder mSurfaceHolder;
private ProgressBar mLiveProgressBar;
private RelativeLayout mRlControl;
private LinearLayout mLlHc;
private ImageButton mIbRotate2;
private RelativeLayout mRaTitle;
// 从Intent中获取的参数
private String mAppKey;
private String mDeviceSerial;
private String mVerifyCode;
private String mAccessToken;
private int mCameraNo = 0; // 默认通道号0
private Handler mHandler;
private boolean mP2PEnabled = true;
// 新增:云台控制重试机制相关变量 private static final int MAX_PTZ_RETRIES = 2; private Map<Integer, Integer> ptzRetryCountMap = new ConcurrentHashMap<>(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activitymain); mHandler = new Handler(this); mRaTitle = findViewById(R.id.ra_title); // 1. 从Intent中获取参数 extractParametersFromIntent(); // 2. 初始化UI initUI(); initOrientationSensitiveViews(); View fanHui = findViewById(R.id.back); fanHui.setOnClickListener(v -> finish()); Button huifangBtn = findViewById(R.id.huifang); huifangBtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // 创建Intent跳转到FanHui活动 Intent intent = new Intent(MainActivity.this, FanHui.class); // 传递必要参数(可选) intent.putExtra("deviceSerial", mDeviceSerial); intent.putExtra("cameraNo", mCameraNo); intent.putExtra("accessToken", mAccessToken); intent.putExtra("appkey", mAppKey); intent.putExtra("verifyCode", mVerifyCode); startActivity(intent); } }); // 3. 初始化SDK并创建播放器 initSDKAndCreatePlayer(); } private void initOrientationSensitiveViews() { mLiveProgressBar = findViewById(R.id.liveProgressBar); mRlControl = findViewById(R.id.rl_control); mLlHc = findViewById(R.id.ll_hc); mIbRotate2 = findViewById(R.id.ib_rotate2); // 初始状态显示加载 if (mLiveProgressBar != null) { mLiveProgressBar.setVisibility(View.VISIBLE); } // 初始根据方向更新UI updateLayoutByOrientation(); // 创建线程池 mExecutorService = Executors.newFixedThreadPool(2); // 4. 初始化云台控制按钮 initPTZButtons(); } private void initPTZButtons() { // 竖屏布局的按钮 setupPTZButton(R.id.ptz_left_btn, DIRECTION_LEFT); setupPTZButton(R.id.ptz_right_btn, DIRECTION_RIGHT); setupPTZButton(R.id.ptz_top_btn, DIRECTION_UP); setupPTZButton(R.id.ptz_bottom_btn, DIRECTION_DOWN); setupPTZButton(R.id.zoom_add, DIRECTION_ZOOM_IN); // 物理放大 setupPTZButton(R.id.zoom_reduce, DIRECTION_ZOOM_OUT); // 物理缩小 setupPTZButton(R.id.focus_add, DIRECTION_FOCUS_NEAR); // 近焦距 setupPTZButton(R.id.foucus_reduce, DIRECTION_FOCUS_FAR); // 远焦距 setupUnsupportedButton(R.id.guangquan_add, "光圈+"); setupUnsupportedButton(R.id.guangquan_reduce, "光圈-"); // 横屏布局的按钮 setupPTZButton(R.id.ptz_left_btn2, DIRECTION_LEFT); setupPTZButton(R.id.ptz_right_btn2, DIRECTION_RIGHT); setupPTZButton(R.id.ptz_top_btn2, DIRECTION_UP); setupPTZButton(R.id.ptz_bottom_btn2, DIRECTION_DOWN); setupPTZButton(R.id.zoom_add2, DIRECTION_ZOOM_IN); setupPTZButton(R.id.zoom_reduce2, DIRECTION_ZOOM_OUT); setupPTZButton(R.id.focus_add2, DIRECTION_FOCUS_NEAR); // 近焦距 setupPTZButton(R.id.foucus_reduce2, DIRECTION_FOCUS_FAR); // 远焦距 } private void setupUnsupportedButton(int buttonId, String buttonName) { ImageButton button = findViewById(buttonId); if (button != null) { button.setOnClickListener(v -> { Toast.makeText(MainActivity.this, "SDK不支持此功能", Toast.LENGTH_SHORT).show(); }); } } private void setupPTZButton(int buttonId, final int direction) { ImageButton button = findViewById(buttonId); if (button != null) { button.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: // 按下时开始云台动作 controlPTZ(direction, SPEED_MEDIUM, true); return true; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: // 如果这是当前活动的方向,则停止 if (isPTZActive && activeDirection == direction) { controlPTZ(direction, SPEED_MEDIUM, false); } return true; } return false; } }); } } // 重构后的云台控制方法 private void controlPTZ(final int direction, final int speed, final boolean isStart) { // 检查是否允许开始新操作 if (isStart) { if (isPTZActive) { // 如果有活动操作,先停止当前操作 controlPTZ(activeDirection, speed, false); // 延迟100ms后再开始新操作 new Handler().postDelayed(() -> controlPTZ(direction, speed, true), 100); return; } } // 重试次数检查 Integer retryCount = ptzRetryCountMap.getOrDefault(direction, 0); if (retryCount >= MAX_PTZ_RETRIES) { Log.w(TAG, "达到最大重试次数,放弃操作 direction=" + direction); ptzRetryCountMap.remove(direction); resetPTZState(); return; } mExecutorService.execute(() -> { try { String urlStr = isStart ? PTZ_START_URL : PTZ_STOP_URL; URL url = new URL(urlStr); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("POST"); conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); conn.setDoOutput(true); conn.setConnectTimeout(5000); conn.setReadTimeout(5000); // 构建POST数据 StringBuilder postData = new StringBuilder(); postData.append("accessToken=").append(URLEncoder.encode(mAccessToken, "UTF-8")); postData.append("&deviceSerial=").append(URLEncoder.encode(mDeviceSerial, "UTF-8")); postData.append("&channelNo=").append(mCameraNo); if (isStart) { postData.append("&direction=").append(direction); postData.append("&speed=").append(speed); } else { // 停止时带上方向参数(建议) postData.append("&direction=").append(direction); } // 发送请求 OutputStream os = conn.getOutputStream(); os.write(postData.toString().getBytes("UTF-8")); os.flush(); os.close(); // 处理响应 int responseCode = conn.getResponseCode(); if (responseCode == HttpURLConnection.HTTP_OK) { // 读取响应内容 InputStream is = conn.getInputStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(is)); StringBuilder response = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { response.append(line); } reader.close(); // 解析JSON响应 JSONObject json = new JSONObject(response.toString()); String code = json.optString("code", "0"); String msg = json.optString("msg", "未知错误"); if ("200".equals(code)) { Log.d(TAG, "PTZ控制成功: " + (isStart ? "开始" : "停止") + " | 方向: " + direction); // 更新状态 if (isStart) { isPTZActive = true; activeDirection = direction; } else { isPTZActive = false; activeDirection = -1; } // 成功时重置重试计数器 ptzRetryCountMap.remove(direction); } else { // 增加重试计数 ptzRetryCountMap.put(direction, retryCount + 1); if (isStart) { Log.w(TAG, "PTZ控制失败,尝试重试 direction=" + direction + ", retry=" + (retryCount + 1)); controlPTZ(direction, speed, true); } else { // 停止命令失败需要特殊处理 handleStopFailure(direction); } handlePTZError(code, msg, isStart); } } else { // 增加重试计数 ptzRetryCountMap.put(direction, retryCount + 1); if (isStart) { Log.w(TAG, "PTZ控制失败,尝试重试 direction=" + direction + ", retry=" + (retryCount + 1)); controlPTZ(direction, speed, true); } else { // 停止命令失败需要特殊处理 handleStopFailure(direction); } Log.e(TAG, "HTTP错误: " + responseCode); runOnUiThread(() -> Toast.makeText(MainActivity.this, "网络请求失败: " + responseCode, Toast.LENGTH_SHORT).show()); } conn.disconnect(); } catch (Exception e) { // 增加重试计数 ptzRetryCountMap.put(direction, retryCount + 1); if (isStart && retryCount < MAX_PTZ_RETRIES) { Log.w(TAG, "PTZ控制异常,尝试重试", e); controlPTZ(direction, speed, true); } else { handleStopFailure(direction); } Log.e(TAG, "PTZ控制异常", e); runOnUiThread(() -> Toast.makeText(MainActivity.this, "云台控制出错: " + e.getMessage(), Toast.LENGTH_SHORT).show()); } }); } // 新增:处理停止失败的情况 private void handleStopFailure(int direction) { runOnUiThread(() -> { // 紧急停止:发送不带方向参数的停止命令 emergencyStopPTZ(); Toast.makeText(MainActivity.this, "云台停止失败,已执行紧急停止", Toast.LENGTH_SHORT).show(); }); // 重置状态 resetPTZState(); } // 新增:紧急停止方法(不带方向参数) private void emergencyStopPTZ() { mExecutorService.execute(() -> { try { URL url = new URL(PTZ_STOP_URL); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("POST"); conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); conn.setDoOutput(true); conn.setConnectTimeout(5000); conn.setReadTimeout(5000); // 构建不带方向参数的POST数据 StringBuilder postData = new StringBuilder(); postData.append("accessToken=").append(URLEncoder.encode(mAccessToken, "UTF-8")); postData.append("&deviceSerial=").append(URLEncoder.encode(mDeviceSerial, "UTF-8")); postData.append("&channelNo=").append(mCameraNo); // 发送请求 OutputStream os = conn.getOutputStream(); os.write(postData.toString().getBytes("UTF-8")); os.flush(); os.close(); // 处理响应 int responseCode = conn.getResponseCode(); if (responseCode == HttpURLConnection.HTTP_OK) { // 读取响应内容 InputStream is = conn.getInputStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(is)); StringBuilder response = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { response.append(line); } reader.close(); // 解析JSON响应 JSONObject json = new JSONObject(response.toString()); String code = json.optString("code", "0"); if ("200".equals(code)) { Log.d(TAG, "紧急停止成功"); } else { Log.w(TAG, "紧急停止失败: " + json.optString("msg", "未知错误")); } } else { Log.e(TAG, "紧急停止HTTP错误: " + responseCode); } conn.disconnect(); } catch (Exception e) { Log.e(TAG, "紧急停止异常", e); } }); } // 新增:重置云台状态 private void resetPTZState() { isPTZActive = false; activeDirection = -1; ptzRetryCountMap.clear(); } private void handlePTZError(String code, String msg, boolean isStart) { String errorMessage = msg; // 根据文档映射错误信息 switch (code) { case "10001": errorMessage = "参数错误"; break; case "10002": errorMessage = "accessToken异常或过期"; break; case "20002": errorMessage = "设备不存在"; break; case "20007": errorMessage = "设备不在线"; break; case "60000": errorMessage = "设备不支持云台控制"; mIsSupportPTZ = false; // 更新设备支持状态 break; case "60001": errorMessage = "用户无云台控制权限"; break; case "60020": errorMessage = "不支持该命令"; break; // 添加其他错误码处理... } final String finalMsg = errorMessage; runOnUiThread(() -> { Toast.makeText(MainActivity.this, (isStart ? "开始" : "停止") + "云台控制失败: " + finalMsg, Toast.LENGTH_LONG).show(); // 对于特定错误,重置云台状态 if ("10002".equals(code) || "60000".equals(code)) { isPTZActive = false; activeDirection = -1; } }); } @Override public void onConfigurationChanged(@NonNull Configuration newConfig) { super.onConfigurationChanged(newConfig); // 方向变化时重新初始化视图 initOrientationSensitiveViews(); updateLayoutByOrientation(); } @Override protected void onResume() { super.onResume(); if (mLiveProgressBar != null) { if (mIsPlaying) { mLiveProgressBar.setVisibility(View.GONE); } else { mLiveProgressBar.setVisibility(View.VISIBLE); } } } private void updateLayoutByOrientation() { int orientation = getResources().getConfiguration().orientation; if (orientation == Configuration.ORIENTATION_PORTRAIT) { // 竖屏模式 mRaTitle.setVisibility(View.VISIBLE); mRlControl.setVisibility(View.VISIBLE); mLlHc.setVisibility(View.GONE); mIbRotate2.setVisibility(View.GONE); } else { // 横屏模式 mRaTitle.setVisibility(View.GONE); mRlControl.setVisibility(View.GONE); mLlHc.setVisibility(View.VISIBLE); mIbRotate2.setVisibility(View.VISIBLE); } } /** * 从Intent中提取传递的参数 */ private void extractParametersFromIntent() { Bundle extras = getIntent().getExtras(); if (extras != null) { mAppKey = extras.getString(KEY_APPKEY, ""); mDeviceSerial = extras.getString(KEY_SERIAL, ""); mVerifyCode = extras.getString(KEY_VERIFYCODE, ""); mAccessToken = extras.getString(KEY_ACCESSTOKEN, ""); mCameraNo = extras.getInt(KEY_CAMERANO, 0); Log.d(TAG, "Received parameters:"); Log.d(TAG, "AppKey: " + mAppKey); Log.d(TAG, "DeviceSerial: " + mDeviceSerial); Log.d(TAG, "VerifyCode: " + mVerifyCode); Log.d(TAG, "AccessToken: " + mAccessToken); Log.d(TAG, "CameraNo: " + mCameraNo); } else { Log.e(TAG, "No parameters received from intent"); // 如果没有参数,可以显示错误信息并退出 finish(); } } /** * 初始化UI组件 */ private void initUI() { mSurfaceView = findViewById(R.id.realplay_sv); if (mSurfaceView != null) { mSurfaceHolder = mSurfaceView.getHolder(); mSurfaceHolder.addCallback(this); } else { Log.e(TAG, "SurfaceView not found with ID realplay_sv"); } } /** * 初始化SDK并创建播放器 */ private void initSDKAndCreatePlayer() { try { // 1. 初始化SDK EZOpenSDK.initLib(getApplication(), mAppKey); EZOpenSDK.getInstance().setAccessToken(mAccessToken); // +++ 开启P2P取流方式 +++ EZOpenSDK.enableP2P(true); // 开启P2P取流 Log.d(TAG, "P极取流已启用"); mP2PEnabled = true; // 2. 创建播放器 createPlayer(); } catch (Exception e) { Log.e(TAG, "SDK初始化失败", e); Toast.makeText(this, "SDK初始化失败", Toast.LENGTH_SHORT).show(); } } /** * 创建播放器并开始播放 */ private void createPlayer() { try { // 1. 创建播放器实例 mEZPlayer = EZOpenSDK.getInstance().createPlayer(mDeviceSerial, mCameraNo); // 2. 配置播放器 mEZPlayer.setHandler(mHandler); if (mSurfaceHolder != null) { mEZPlayer.setSurfaceHold(mSurfaceHolder); } if (mVerifyCode != null && !mVerifyCode.isEmpty()) { mEZPlayer.setPlayVerifyCode(mVerifyCode); } Toast.makeText(MainActivity.this, "连接成功", Toast.LENGTH_SHORT).show(); // 3. 开始播放 mEZPlayer.startRealPlay(); mIsPlaying = true; // 标记为正在播放 } catch (Exception e) { Log.e(TAG, "Player creation failed", e); mIsPlaying = false; // 标记为未播放 } } // 处理屏幕旋转按钮点击 public void changeScreen(View view) { Log.d(TAG, "Change screen orientation requested"); int currentOrientation = getResources().getConfiguration().orientation; if (currentOrientation == Configuration.ORIENTATION_PORTRAIT) { setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); } else { setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); } // 更新UI布局 updateLayoutByOrientation(); } // Surface回调接口实现 @Override public void surfaceCreated(@NonNull SurfaceHolder holder) { if (mEZPlayer != null) { mEZPlayer.setSurfaceHold(holder); } } @Override public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {} @Override public void surfaceDestroyed(@NonNull SurfaceHolder holder) { if (mEZPlayer != null) { mEZPlayer.setSurfaceHold(null); } } @Override protected void onStop() { super.onStop(); // 确保云台停止 if (isPTZActive && activeDirection != -1) { Log.d(TAG, "Activity停止,强制停止云台"); controlPTZ(activeDirection, SPEED_MEDIUM, false); } if (mEZPlayer != null) { mEZPlayer.stopRealPlay(); mIsPlaying = false; // 标记为已停止 } } // Handler回调处理播放状态 @Override public boolean handleMessage(@NonNull Message msg) { Log.d(TAG, "handleMessage: " + msg.what); switch (msg.what) { case MSG_VIDEO_SIZE_CHANGED: break; case MSG_REALPLAY_PLAY_SUCCESS: Log.i(TAG, "播放成功"); mIsPlaying = true; // 获取并显示取流方式 int streamType = mEZPlayer.getStreamFetchType(); String streamTypeName = getStreamTypeName(streamType); Log.d(TAG, "当前取流方式: " + streamTypeName); // 发送消息显示取流方式 Message showMsg = new Message(); showMsg.what = MSG_SHOW_STREAM_TYPE; showMsg.obj = streamTypeName; mHandler.sendMessage(showMsg); runOnUiThread(() -> { if (mLiveProgressBar != null) { mLiveProgressBar.setVisibility(View.GONE); } if (!mHasShownConnectionSuccess) { mHasShownConnectionSuccess = true; } }); // 在播放成功后检查设备是否支持云台 checkDevicePTZSupport(); break; case MSG_REALPLAY_PLAY_FAIL: Log.e(TAG, "播放失败"); mIsPlaying = false; runOnUiThread(() -> { if (mLiveProgressBar != null) { mLiveProgressBar.setVisibility(View.VISIBLE); } }); BaseException error = (BaseException) msg.obj; int errorCode = error.getErrorCode(); if (errorCode == ErrorCode.ERROR_INNER_VERIFYCODE_NEED || errorCode == ErrorCode.ERROR_INNER_VERIFYCODE_ERROR) { mVerifyCode = "123456"; if (mEZPlayer != null) { mEZPlayer.setPlayVerifyCode(mVerifyCode); mEZPlayer.startRealPlay(); } } else { Log.e(TAG, "播放失败,错误码: " + errorCode); } break; case MSG_SHOW_STREAM_TYPE: String type = (String) msg.obj; Toast.makeText(MainActivity.this, "取流方式: " + type + (mP2PEnabled ? " (P2P已启用)" : ""), Toast.LENGTH_LONG).show(); break; } return true; } private void checkDevicePTZSupport() { new Thread(() -> { try { // 这里使用SDK方法检查设备能力 // 如果SDK没有提供方法,可以使用HTTP API查询设备能力 // 简化处理:假设所有设备都支持云台 mIsSupportPTZ = true; // 实际项目中应该查询设备能力 // mIsSupportPTZ = queryDevicePTZCapability(); } catch (Exception e) { Log.e(TAG, "Failed to check PTZ support", e); } }).start(); } @Override protected void onDestroy() { super.onDestroy(); if (mExecutorService != null) { mExecutorService.shutdown(); } // 移除Handler回调避免内存泄漏 if (mHandler != null) { mHandler.removeCallbacksAndMessages(null); } if (mEZPlayer != null) { mEZPlayer.release(); mEZPlayer = null; } } private String getStreamTypeName(int type) { switch (type) { case 0: return "流媒体"; case 1: return "P2P"; case 2: return "内网直连"; case 4: return "云存储"; default: return "未知(" + type + ")"; } }
}
FanHui.java:package com.videogo.ui.login;
import android.app.AlertDialog;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.DatePicker;
import android.widget.ImageButton;
import android.widget.ListView;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
import com.videogo.exception.BaseException;
import com.videogo.exception.ErrorCode;
import com.videogo.openapi.EZConstants;
import com.videogo.openapi.EZPlayer;
import androidx.appcompat.app.AppCompatActivity;
import com.videogo.openapi.EZOpenSDK;
import ezviz.ezopensdk.R;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class FanHui extends AppCompatActivity implements SurfaceHolder.Callback {
private static final String TAG = "EZPlayback"; private String mAppKey; private String mDeviceSerial; private String mVerifyCode; private String mAccessToken; private int mCameraNo; private TextView mDateTextView; private int mSelectedYear, mSelectedMonth, mSelectedDay; private static final String KEY_APPKEY = "appkey"; private static final String KEY_SERIAL = "serial"; private static final String KEY_VERIFYCODE = "VerifyCode"; private static final String KEY_ACCESSTOKEN = "accessToken"; private static final String KEY_CAMERANO = "cameraNo"; // 回放录像相关 private static final String VIDEO_BY_TIME_URL = "https://open.ys7.com/api/lapp/video/by/time"; private ExecutorService mExecutorService; private ListView mListView; private PlaybackAdapter mAdapter; private List<VideoInfo> mVideoList = new ArrayList<>(); // 播放器相关 private EZPlayer mEZPlayer; private SurfaceView mPlaybackSurfaceView; private SurfaceHolder mSurfaceHolder; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.ez_playback_list_page); // 创建线程池 mExecutorService = Executors.newFixedThreadPool(2); extractParametersFromIntent(); final Calendar calendar = Calendar.getInstance(); mSelectedYear = calendar.get(Calendar.YEAR); mSelectedMonth = calendar.get(Calendar.MONTH); mSelectedDay = calendar.get(Calendar.DAY_OF_MONTH); // 初始化视图 initViews(); // 设置日期显示模块 setupDatePicker(); // 默认加载当天的录像 loadVideosForSelectedDate(); // 初始化SDK initSDK(); } private void initSDK() { try { // 初始化萤石云SDK EZOpenSDK.initLib(getApplication(), mAppKey); EZOpenSDK.getInstance().setAccessToken(mAccessToken); } catch (Exception e) { Log.e(TAG, "SDK初始化失败", e); Toast.makeText(this, "SDK初始化失败", Toast.LENGTH_SHORT).show(); } } private void initViews() { // 查找ListView mListView = findViewById(R.id.listView); if (mListView == null) { Log.e(TAG, "ListView not found with ID listView"); return; } // 初始化适配器 mAdapter = new PlaybackAdapter(); mListView.setAdapter(mAdapter); // 设置点击事件 mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { VideoInfo video = mVideoList.get(position); playVideo(video); } }); // 获取播放器视图 - 使用SurfaceView mPlaybackSurfaceView = findViewById(R.id.remote_playback_wnd_sv); if (mPlaybackSurfaceView != null) { // 设置SurfaceHolder回调 mSurfaceHolder = mPlaybackSurfaceView.getHolder(); mSurfaceHolder.addCallback(this); } else { Log.e(TAG, "SurfaceView not found with ID remote_playback_wnd_sv"); } } // SurfaceHolder回调方法 @Override public void surfaceCreated(SurfaceHolder holder) { Log.d(TAG, "Surface created"); // 当Surface创建时,如果有播放器实例,设置SurfaceHolder if (mEZPlayer != null) { mEZPlayer.setSurfaceHold(holder); } } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { Log.d(TAG, "Surface changed: " + width + "x" + height); } @Override public void surfaceDestroyed(SurfaceHolder holder) { Log.d(TAG, "Surface destroyed"); // 当Surface销毁时,释放播放器资源 stopPlayback(); } private void setupDatePicker() { mDateTextView = findViewById(R.id.date_text); ImageButton datePickerButton = findViewById(R.id.date_picker_button); updateDateDisplay(); datePickerButton.setOnClickListener(v -> showDatePickerDialog()); } private void updateDateDisplay() { String formattedDate = String.format(Locale.getDefault(), "%d年%02d月%02d日", mSelectedYear, mSelectedMonth + 1, // 月份需要+1 mSelectedDay); mDateTextView.setText(formattedDate); } private void showDatePickerDialog() { final AlertDialog dlg = new AlertDialog.Builder(this, R.style.Theme_AppCompat_Dialog).create(); dlg.show(); Window window = dlg.getWindow(); window.setContentView(R.layout.datepicker_layout); // 设置对话框宽度 WindowManager.LayoutParams lp = window.getAttributes(); lp.width = WindowManager.LayoutParams.MATCH_PARENT; window.setAttributes(lp); // 获取并初始化 DatePicker DatePicker dpPicker = window.findViewById(R.id.dpPicker); // 隐藏不需要的视图 ViewGroup rootView = (ViewGroup) dpPicker.getChildAt(0); if (rootView != null) { ViewGroup childView = (ViewGroup) rootView.getChildAt(0); if (childView != null) { childView.getChildAt(2).setVisibility(View.VISIBLE); // 确保月选择器可见 childView.getChildAt(1).setVisibility(View.VISIBLE); } } dpPicker.init(mSelectedYear, mSelectedMonth, mSelectedDay, null); // 设置按钮事件 RelativeLayout yesButton = window.findViewById(R.id.YES); RelativeLayout noButton = window.findViewById(R.id.NO); yesButton.setOnClickListener(v -> { mSelectedYear = dpPicker.getYear(); mSelectedMonth = dpPicker.getMonth(); mSelectedDay = dpPicker.getDayOfMonth(); updateDateDisplay(); dlg.dismiss(); // 加载新选择的日期的录像 loadVideosForSelectedDate(); }); noButton.setOnClickListener(v -> dlg.dismiss()); } private void extractParametersFromIntent() { Bundle extras = getIntent().getExtras(); if (extras != null) { mAppKey = extras.getString(KEY_APPKEY, ""); mDeviceSerial = extras.getString(KEY_SERIAL, ""); mVerifyCode = extras.getString(KEY_VERIFYCODE, ""); mAccessToken = extras.getString(KEY_ACCESSTOKEN, ""); mCameraNo = extras.getInt(KEY_CAMERANO, 0); Log.d(TAG, "Received parameters:"); Log.d(TAG, "AppKey: " + mAppKey); Log.d(TAG, "DeviceSerial: " + mDeviceSerial); Log.d(TAG, "VerifyCode: " + mVerifyCode); Log.d(TAG, "AccessToken: " + mAccessToken); Log.d(TAG, "CameraNo: " + mCameraNo); } else { Log.e(TAG, "No parameters received from intent"); } } private void loadVideosForSelectedDate() { // 计算开始和结束时间戳 Calendar cal = Calendar.getInstance(); cal.set(mSelectedYear, mSelectedMonth, mSelectedDay, 0, 0, 0); long startTime = cal.getTimeInMillis(); cal.set(mSelectedYear, mSelectedMonth, mSelectedDay, 23, 59, 59); long endTime = cal.getTimeInMillis(); // 发起网络请求获取录像 fetchVideosByTime(startTime, endTime); } private void fetchVideosByTime(long startTime, long endTime) { mExecutorService.execute(() -> { try { URL url = new URL(VIDEO_BY_TIME_URL); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("POST"); conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); conn.setDoOutput(true); conn.setConnectTimeout(10000); conn.setReadTimeout(10000); // 构建POST数据(添加分页参数) StringBuilder postData = new StringBuilder(); postData.append("accessToken=").append(URLEncoder.encode(mAccessToken, "UTF-8")); postData.append("&deviceSerial=").append(URLEncoder.encode(mDeviceSerial, "UTF-8")); postData.append("&channelNo=").append(mCameraNo); postData.append("&startTime=").append(startTime); postData.append("&endTime=").append(endTime); postData.append("&recType=").append(0); // 系统自动选择 postData.append("&version=2.0"); // 添加分页版本 postData.append("&pageSize=100"); // 添加分页大小 // 发送请求 OutputStream os = conn.getOutputStream(); os.write(postData.toString().getBytes("UTF-8")); os.flush(); os.close(); // 处理响应 int responseCode = conn.getResponseCode(); if (responseCode == HttpURLConnection.HTTP_OK) { // 读取响应内容 InputStream is = conn.getInputStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(is)); StringBuilder response = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { response.append(line); } reader.close(); // 解析JSON响应 JSONObject json = new JSONObject(response.toString()); String code = json.optString("code", "0"); if ("200".equals(code)) { JSONArray data = json.getJSONArray("data"); List<VideoInfo> videos = parseVideoData(data); runOnUiThread(() -> { mVideoList.clear(); mVideoList.addAll(videos); mAdapter.notifyDataSetChanged(); }); } else { String msg = json.optString("msg", "未知错误"); Log.e(TAG, "获取录像失败: " + msg); runOnUiThread(() -> Toast.makeText(FanHui.this, "获取录像失败: " + msg, Toast.LENGTH_SHORT).show()); } } else { Log.e(TAG, "HTTP错误: " + responseCode); runOnUiThread(() -> Toast.makeText(FanHui.this, "网络请求失败: " + responseCode, Toast.LENGTH_SHORT).show()); } conn.disconnect(); } catch (Exception e) { Log.e(TAG, "获取录像异常", e); runOnUiThread(() -> Toast.makeText(FanHui.this, "获取录像出错: " + e.getMessage(), Toast.LENGTH_SHORT).show()); } }); } private List<VideoInfo> parseVideoData(JSONArray data) throws JSONException { List<VideoInfo> videos = new ArrayList<>(); SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss", Locale.getDefault()); for (int i = 0; i < data.length(); i++) { JSONObject videoObj = data.getJSONObject(i); VideoInfo video = new VideoInfo(); video.id = videoObj.optString("id"); video.startTime = videoObj.optLong("startTime"); video.endTime = videoObj.optLong("endTime"); video.recType = videoObj.optInt("recType"); // 格式化时间显示 Date startDate = new Date(video.startTime); Date endDate = new Date(video.endTime); video.timeRange = sdf.format(startDate) + " - " + sdf.format(endDate); videos.add(video); } return videos; } private void playVideo(VideoInfo video) { // 停止当前播放 stopPlayback(); try { // 创建播放器实例 mEZPlayer = EZOpenSDK.getInstance().createPlayer(mDeviceSerial, mCameraNo); // 配置播放器 - 设置回调处理器 mEZPlayer.setHandler(mHandler); // 设置验证码 mEZPlayer.setPlayVerifyCode(mVerifyCode); // 关联播放视图 - 使用SurfaceHolder(修复方法名) if (mSurfaceHolder != null) { mEZPlayer.setSurfaceHold(mSurfaceHolder); } else { Log.e(TAG, "无法关联播放视图"); Toast.makeText(this, "播放视图未初始化", Toast.LENGTH_SHORT).show(); } // 创建Calendar对象作为参数 Calendar startCal = Calendar.getInstance(); startCal.setTimeInMillis(video.startTime); Calendar endCal = Calendar.getInstance(); endCal.setTimeInMillis(video.endTime); // 开始回放 mEZPlayer.startPlayback(startCal, endCal); Toast.makeText(this, "开始播放录像: " + video.timeRange, Toast.LENGTH_SHORT).show(); } catch (Exception e) { Log.e(TAG, "播放录像失败", e); Toast.makeText(this, "播放录像失败: " + e.getMessage(), Toast.LENGTH_SHORT).show(); } } private void stopPlayback() { if (mEZPlayer != null) { mEZPlayer.stopPlayback(); mEZPlayer.release(); mEZPlayer = null; } } // 播放器回调处理器 private Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case EZConstants.EZRealPlayConstants.MSG_REALPLAY_PLAY_SUCCESS: Log.i(TAG, "回放播放成功"); break; case EZConstants.EZRealPlayConstants.MSG_REALPLAY_PLAY_FAIL: Log.e(TAG, "回放播放失败"); BaseException error = (BaseException) msg.obj; int errorCode = error.getErrorCode(); String errorMsg = "播放失败: " + errorCode; // 根据错误码提供更具体的错误信息 if (errorCode == ErrorCode.ERROR_INNER_VERIFYCODE_NEED) { errorMsg = "需要验证码"; } else if (errorCode == ErrorCode.ERROR_INNER_VERIFYCODE_ERROR) { errorMsg = "验证码错误"; } else if (errorCode == ErrorCode.ERROR_TRANSF_ACCESSTOKEN_ERROR) { errorMsg = "accessToken无效"; } Toast.makeText(FanHui.this, errorMsg, Toast.LENGTH_LONG).show(); break; } } }; @Override protected void onDestroy() { super.onDestroy(); stopPlayback(); if (mExecutorService != null) { mExecutorService.shutdown(); } } // 录像信息数据结构 private static class VideoInfo { String id; long startTime; long endTime; int recType; String timeRange; } // 列表适配器 private class PlaybackAdapter extends BaseAdapter { @Override public int getCount() { return mVideoList.size(); } @Override public Object getItem(int position) { return mVideoList.get(position); } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder; if (convertView == null) { convertView = getLayoutInflater().inflate(R.layout.video_item_layout, parent, false); holder = new ViewHolder(); holder.timeTextView = convertView.findViewById(R.id.time_text); holder.durationTextView = convertView.findViewById(R.id.duration_text); convertView.setTag(holder); } else { holder = (ViewHolder) convertView.getTag(); } VideoInfo video = mVideoList.get(position); // 计算持续时间(分钟) long durationMinutes = (video.endTime - video.startTime) / (1000 * 60); holder.timeTextView.setText(video.timeRange); holder.durationTextView.setText(durationMinutes + "分钟"); return convertView; } class ViewHolder { TextView timeTextView; TextView durationTextView; } }
}