活动介绍

Error:(32, 17) java: 对于invokeAll(ss), 找不到合适的方法 方法 java.util.concurrent.ExecutorService.<T>invokeAll(java.util.Collection<? extends java.util.concurrent.Callable<T>>)不适用 (无法推断类型变量 T (参数不匹配; ss无法转换为java.util.Collection<? extends java.util.concurrent.Callable<T>>)) 方法 java.util.concurrent.ExecutorService.<T>invokeAll(java.util.Collection<? extends java.util.concurrent.Callable<T>>,long,java.util.concurrent.TimeUnit)不适用 (无法推断类型变量 T (实际参数列表和形式参数列表长度不同))

时间: 2024-03-06 22:49:43 浏览: 146
这个报错信息提示在第32行中的invokeAll方法调用参数不匹配,无法找到合适的方法。可能有两个问题: 1. ss类没有实现Callable接口,导致无法将其转换为Callable类型的集合,所以参数不匹配。 2. invokeAll方法需要传入一个Callable的集合,而你只传入了一个ss对象,和一个ss类型的集合也无法匹配,所以报错。 你可以将ss改为实现Callable接口,然后将其放入一个Callable类型的集合中,再将集合传入invokeAll方法中。代码示例如下: ``` public class test { public static void main(String[] args) throws InterruptedException { // 创建一个固定大小的线程池 ExecutorService service = Executors.newFixedThreadPool(3); Callable<String> t1 = new ss(); List<Callable<String>> list = new ArrayList<>(); list.add(t1); service.invokeAll(list); // 关闭启动线程 service.shutdown(); // 等待子线程结束,再继续执行下面的代码 service.awaitTermination(Long.MAX_VALUE, TimeUnit.DAYS); //System.out.println("all thread complete"); } } class ss implements Callable<String> { @Override public String call() { System.out.println(Thread.currentThread().getName() + "执行call方法"); return null; } } ``` 这样就可以避免参数不匹配的问题了。
阅读全文

相关推荐

cachemanager 是null2025-06-18 14:35:45.733 INFO 1 [main] com.github.yulichang.autoconfigure.MybatisPlusJoinAutoConfiguration MPJSqlInjector init 2025-06-18 14:35:45.760 WARN 1 [main] org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'cacheManager' defined in class path resource [com/lzkj/common/redis/config/RedisConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.cache.CacheManager]: Factory method 'cacheManager' threw exception; nested exception is java.lang.NullPointerException 2025-06-18 14:35:45.764 ERROR 1 [main] com.xxl.job.core.executor.XxlJobExecutor null java.lang.NullPointerException: null at com.xxl.job.core.executor.XxlJobExecutor.stopEmbedServer(XxlJobExecutor.java:158) at com.xxl.job.core.executor.XxlJobExecutor.destroy(XxlJobExecutor.java:85) at com.xxl.job.core.executor.impl.XxlJobSpringExecutor.destroy(XxlJobSpringExecutor.java:55) at org.springframework.beans.factory.support.DisposableBeanAdapter.destroy(DisposableBeanAdapter.java:258) at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.destroyBean(DefaultSingletonBeanRegistry.java:571) at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.destroySingleton(DefaultSingletonBeanRegistry.java:543) at org.springframework.beans.factory.support.DefaultListableBeanFactory.destroySingleton(DefaultListableBeanFactory.java:1072) at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.destroySingletons(DefaultSingletonBeanRegistry.java:504) at org.springframework.beans.factory.support.DefaultListableBeanFactory.destroySingletons(DefaultListableBeanFactory.java:1065) at org.springframework.context.support.AbstractApplicationContext.destroyBeans(AbstractApplicationContext.java:1060) at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:563) at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:141) at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:747) at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:397) at org.springframework.boot.SpringApplication.run(SpringApplication.java:315) at org.springframework.boot.SpringApplication.run(SpringApplication.java:1226) at org.springframework.boot.SpringApplication.run(SpringApplication.java:1215) at com.lz.ss.platform.live.server.LiveServerApplication.main(LiveServerApplication.java:21) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.springframework.boot.loader.MainMethodRunner.run(MainMethodRunner.java:48) at org.springframework.boot.loader.Launcher.launch(Launcher.java:87) at org.springframework.boot.loader.Launcher.launch(Launcher.java:51) at org.springframework.boot.loader.JarLauncher.main(JarLauncher.java:52) 2025-06-18 14:35:45.769 INFO 1 [main] org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor Shutting down ExecutorService 'threadPoolTaskExecutor' 2025-06-18 14:35:45.779 INFO 1 [main] com.alibaba.druid.pool.DruidDataSource {dataSource-0} closing ... 2025-06-18 14:35:45.779 INFO 1 [main] com.alibaba.druid.pool.DruidDataSource {dataSource-0} closing ... 2025-06-18 14:35:45.825 INFO 1 [main] org.apache.catalina.core.StandardService Stopping service [Tomcat] 2025-06-18 14:35:45.956 WARN 1 [main] org.apache.catalina.loader.WebappClassLoaderBase The web application [live] appears to have started a thread named [ForkJoinPool.commonPool-worker-1] but has failed to stop it. This is very likely to create a memory leak. Stack trace of thread: sun.misc.Unsafe.park(Native Method) java.util.concurrent.ForkJoinPool.awaitWork(ForkJoinPool.java:1824) java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1693) java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:172) 2025-06-18 14:35:45.976 INFO 1 [main] org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled. 2025-06-18 14:35:45.981 ERROR 1 [main] org.springframework.boot.SpringApplication Application run failed org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'cacheManager' defined in class path resource [com/lzkj/common/redis/config/RedisConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.cache.CacheManager]: Factory method 'cacheManager' threw exception; nested exception is java.lang.NullPointerException at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:656) at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:636) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1338) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1177) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:557) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:517) at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:323) at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:222) at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:321) at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202) at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:879) at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:878) at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:550) at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:141) at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:747) at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:397) at org.springframework.boot.SpringApplication.run(SpringApplication.java:315) at org.springframework.boot.SpringApplication.run(SpringApplication.java:1226) at org.springframework.boot.SpringApplication.run(SpringApplication.java:1215) at com.lz.ss.platform.live.server.LiveServerApplication.main(LiveServerApplication.java:21) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.springframework.boot.loader.MainMethodRunner.run(MainMethodRunner.java:48) at org.springframework.boot.loader.Launcher.launch(Launcher.java:87) at org.springframework.boot.loader.Launcher.launch(Launcher.java:51) at org.springframework.boot.loader.JarLauncher.main(JarLauncher.java:52) Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.cache.CacheManager]: Factory method 'cacheManager' threw exception; nested exception is java.lang.NullPointerException at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:185) at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:651) ... 27 common frames omitted Caused by: java.lang.NullPointerException: null at com.lzkj.common.redis.config.RedisConfiguration.cacheManager(RedisConfiguration.java:82) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:154) ... 28 common frames omitted

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.text.TextUtils; 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.LinearLayout; import android.widget.ListView; import android.widget.RelativeLayout; import android.widget.TextView; import android.widget.Toast; import android.content.res.Configuration; import com.videogo.exception.BaseException; import com.videogo.exception.ErrorCode; 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; import java.util.concurrent.TimeUnit; import android.content.pm.ActivityInfo; import com.videogo.widget.CheckTextButton; 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 = "deviceSerial"; private static final String KEY_VERIFYCODE = "verifyCode"; private static final String KEY_ACCESSTOKEN = "accessToken"; private static final String KEY_CAMERANO = "cameraNo"; private static final int MSG_PLAYBACK_PLAY_SUCCESS = 3001; private static final int MSG_PLAYBACK_PLAY_FAIL = 3002; private static final int MSG_PLAYBACK_STOP = 3003; // 更新API地址为云存储专用接口 private static final String VIDEO_BY_TIME_URL = "https://open.ys7.com/api/lapp/video/by/time"; // private static final String PLAYBACK_URL_API = "https://open.ys7.com/api/lapp/v2/live/address/get"; private boolean isFullScreen = false; private com.videogo.widget.TitleBar mTitleBar; private ViewGroup mPlaybackArea; private LinearLayout mControlArea; private RelativeLayout mDisplayLayout; private ImageButton mExitFullScreenButton; private ExecutorService mExecutorService; private ListView mListView; private PlaybackAdapter mAdapter; private List<VideoInfo> mVideoList = new ArrayList<>(); private LinearLayout mNoVideoLayout; // 播放器相关 private EZPlayer mEZPlayer; private SurfaceView mPlaybackSurfaceView; private SurfaceHolder mSurfaceHolder; private boolean isVideoPlaying = false; // 跟踪视频播放状态 private ImageButton mPauseButton; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.ez_playback_list_page); View fanHui = findViewById(R.id.fanhui); fanHui.setOnClickListener(v -> finish()); // 创建线程池 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(); mPlaybackArea = findViewById(R.id.remote_playback_area); mControlArea = findViewById(R.id.control_area); mDisplayLayout = findViewById(R.id.display_layout); mExitFullScreenButton = findViewById(R.id.exit_btn); // 设置日期显示模块 setupDatePicker(); // 使用MainActivity已初始化的SDK实例 EZOpenSDK.getInstance().setAccessToken(mAccessToken); // 默认加载当天的录像 loadVideosForSelectedDate(); setupFullscreenButton(); mTitleBar = findViewById(R.id.title); if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) { RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams( RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT ); mPlaybackArea.setLayoutParams(params); } } private void initViews() { // 查找ListView mListView = findViewById(R.id.listView); if (mListView == null) { Log.e(TAG, "ListView not found with ID listView"); return; } mNoVideoLayout = findViewById(R.id.novideo_img_cloud); // 初始化适配器 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); mPauseButton = findViewById(R.id.remote_playback_pause_btn); mPauseButton.setOnClickListener(v -> togglePlayPause()); if (mPlaybackSurfaceView != null) { // 设置SurfaceHolder回调 mSurfaceHolder = mPlaybackSurfaceView.getHolder(); mSurfaceHolder.addCallback(this); } else { Log.e(TAG, "SurfaceView not found with ID remote_playback_wnd_sv"); } } private void togglePlayPause() { if (mEZPlayer == null) return; if (isVideoPlaying) { // 当前正在播放 → 暂停 try { mEZPlayer.pausePlayback(); mPauseButton.setImageResource(R.drawable.play_full_play_sel); // 切换为播放图标 isVideoPlaying = false; } catch (Exception e) { Log.e(TAG, "暂停播放失败", e); } } else { // 当前已暂停 → 继续播放 try { mEZPlayer.resumePlayback(); mPauseButton.setImageResource(R.drawable.play_full_pause_sel); // 切换为暂停图标 isVideoPlaying = true; } catch (Exception e) { Log.e(TAG, "继续播放失败", e); } } } // 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) { // 确保所有选择器组件可见 for (int i = 0; i < childView.getChildCount(); i++) { childView.getChildAt(i).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, "").toUpperCase(); mVerifyCode = extras.getString(KEY_VERIFYCODE, ""); mAccessToken = extras.getString(KEY_ACCESSTOKEN, ""); mCameraNo = extras.getInt(KEY_CAMERANO, 1); // 默认为通道1 Log.d(TAG, "Received parameters:"); Log.d(TAG, "AppKey: " + mAppKey); Log.d(TAG, "DeviceSerial (Upper): " + 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() { if (TextUtils.isEmpty(mDeviceSerial) || TextUtils.isEmpty(mAccessToken)) { Toast.makeText(this, "设备参数不完整", Toast.LENGTH_SHORT).show(); return; } // 计算开始和结束时间戳 Calendar cal = Calendar.getInstance(); cal.set(mSelectedYear, mSelectedMonth, mSelectedDay, 0, 0, 0); long startTime = cal.getTimeInMillis(); long maxEndTime = System.currentTimeMillis(); cal.set(mSelectedYear, mSelectedMonth, mSelectedDay, 23, 59, 59); long endTime = Math.min(cal.getTimeInMillis(), maxEndTime); // 发起网络请求获取录像 fetchVideosByTime(startTime, endTime); } private void fetchVideosByTime(long startTime, long endTime) { if (isFinishing() || isDestroyed()) return; // 加载开始时隐藏无录像提示 runOnUiThread(() -> { if (mNoVideoLayout != null) { mNoVideoLayout.setVisibility(View.GONE); } if (!isFinishing()) { Toast.makeText(FanHui.this, "正在加载可播录像", Toast.LENGTH_SHORT).show(); } }); final String deviceSerial = mDeviceSerial.toUpperCase(); mExecutorService.execute(() -> { HttpURLConnection conn = null; try { String currentUrl = VIDEO_BY_TIME_URL; int redirectCount = 0; final int MAX_REDIRECTS = 3; while (redirectCount < MAX_REDIRECTS) { URL url = new URL(currentUrl); conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("POST"); conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); conn.setDoOutput(true); conn.setConnectTimeout(15000); conn.setReadTimeout(30000); // 构建POST数据 StringBuilder postData = new StringBuilder(); postData.append("accessToken=").append(URLEncoder.encode(mAccessToken, "UTF-8")); postData.append("&deviceSerial=").append(URLEncoder.encode(deviceSerial, "UTF-8")); postData.append("&channelNo=").append(mCameraNo); postData.append("&startTime=").append(startTime); postData.append("&endTime=").append(endTime); postData.append("&recType=0"); // 1-云存储录像 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_MOVED_TEMP || responseCode == HttpURLConnection.HTTP_MOVED_PERM || responseCode == HttpURLConnection.HTTP_SEE_OTHER) { String newUrl = conn.getHeaderField("Location"); if (newUrl == null) break; currentUrl = newUrl; redirectCount++; conn.disconnect(); continue; } // 处理正常响应 if (responseCode == HttpURLConnection.HTTP_OK) { InputStream is = conn.getInputStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(is, "UTF-8")); // 明确指定UTF-8编码 StringBuilder response = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { response.append(line); } reader.close(); String responseString = response.toString(); Log.d(TAG, "API响应: " + responseString); // 记录原始响应 // 检查是否是HTML响应 if (responseString.startsWith("<html>") || responseString.startsWith("<!DOCTYPE")) { Log.e(TAG, "服务器返回HTML: " + responseString); runOnUiThread(() -> { if (!isFinishing()) { Toast.makeText(FanHui.this, "服务器返回错误页面", Toast.LENGTH_LONG).show(); } }); return; } // 解析JSON响应 try { JSONObject json = new JSONObject(responseString); String code = json.optString("code", "0"); if ("200".equals(code)) { Object dataObj = json.opt("data"); List<VideoInfo> videos = new ArrayList<>(); if (dataObj == null) { // 处理data为null的情况(无录像) Log.w(TAG, "API返回data为null,表示没有录像"); } else if (dataObj instanceof JSONArray) { // 处理数组格式响应 JSONArray dataArray = (JSONArray) dataObj; videos = parseVideoData(dataArray); } else if (dataObj instanceof JSONObject) { // 处理分页格式响应 JSONObject dataObject = (JSONObject) dataObj; // 使用 optJSONArray 安全处理 null 值 JSONArray filesArray = dataObject.optJSONArray("files"); if (filesArray != null) { videos = parseVideoData(filesArray); } else { JSONArray recordsArray = dataObject.optJSONArray("records"); if (recordsArray != null) { videos = parseVideoData(recordsArray); } else { JSONArray listArray = dataObject.optJSONArray("list"); if (listArray != null) { videos = parseVideoData(listArray); } else { Log.w(TAG, "分页结构缺少有效的录像数组字段"); // 尝试从其他可能字段获取 if (dataObject.has("videoList")) { videos = parseVideoData(dataObject.optJSONArray("videoList")); } else { Log.w(TAG, "API 返回的分页结构不包含有效录像数据: " + dataObject.toString()); } } } } } else { Log.w(TAG, "未知的data类型: " + dataObj.getClass().getSimpleName()); // 尝试强制转换 try { videos = parseVideoData(new JSONArray().put(dataObj)); } catch (Exception e) { Log.e(TAG, "强制转换失败", e); } } // 更新UI显示录像列表 List<VideoInfo> finalVideos = videos; runOnUiThread(() -> { mVideoList.clear(); mVideoList.addAll(finalVideos); mAdapter.notifyDataSetChanged(); // 根据录像数据控制提示布局可见性 if (finalVideos.isEmpty()) { Toast.makeText(FanHui.this, "该时间段没有录像", Toast.LENGTH_SHORT).show(); if (mNoVideoLayout != null) { mNoVideoLayout.setVisibility(View.VISIBLE); // 显示无录像提示 } } else { if (mNoVideoLayout != null) { mNoVideoLayout.setVisibility(View.GONE); // 隐藏无录像提示 } } }); } else { String msg = json.optString("msg", "未知错误"); Log.e(TAG, "获取录像失败: " + msg); runOnUiThread(() -> Toast.makeText(FanHui.this, "获取录像失败: " + msg, Toast.LENGTH_SHORT).show()); } } catch (JSONException e) { Log.e(TAG, "JSON解析错误: " + e.getMessage(), e); // 添加原始响应内容日志 Log.e(TAG, "原始响应内容: " + responseString); runOnUiThread(() -> Toast.makeText(FanHui.this, "数据格式错误: " + e.getMessage(), Toast.LENGTH_LONG).show()); } break; // 跳出重定向循环 } else { Log.e(TAG, "HTTP错误: " + responseCode); if (!isFinishing() && !isDestroyed()) { runOnUiThread(() -> Toast.makeText(FanHui.this, "网络请求失败: " + responseCode, Toast.LENGTH_SHORT).show()); } break; } } if (redirectCount >= MAX_REDIRECTS) { Log.e(TAG, "重定向次数超过限制"); runOnUiThread(() -> { if (!isFinishing()) { Toast.makeText(FanHui.this, "重定向次数过多", Toast.LENGTH_SHORT).show(); } }); } } catch (Exception e) { Log.e(TAG, "获取录像异常", e); if (!isFinishing() && !isDestroyed()) { runOnUiThread(() -> Toast.makeText(FanHui.this, "获取录像出错: " + e.getMessage(), Toast.LENGTH_SHORT).show()); } } finally { if (conn != null) { conn.disconnect(); } } }); } private List<VideoInfo> parseVideoData(JSONArray records) throws JSONException { List<VideoInfo> videos = new ArrayList<>(); SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss", Locale.getDefault()); for (int i = 0; i < records.length(); i++) { JSONObject record = records.getJSONObject(i); VideoInfo video = new VideoInfo(); // 兼容多种ID字段名 video.id = record.optString("id", record.optString("fileId", record.optString("recordId", "N/A"))); // 兼容多种开始时间字段 video.startTime = record.optLong("startTime", record.optLong("beginTime", record.optLong("start_time", 0))); // 兼容多种结束时间字段 video.endTime = record.optLong("endTime", record.optLong("stopTime", record.optLong("end_time", 0))); // 检查时间有效性 if (video.startTime == 0 || video.endTime == 0) { Log.w(TAG, "跳过无效时间段: start=" + video.startTime + ", end=" + video.endTime); continue; } // 格式化时间显示 try { Date startDate = new Date(video.startTime); Date endDate = new Date(video.endTime); video.timeRange = sdf.format(startDate) + " - " + sdf.format(endDate); videos.add(video); } catch (Exception e) { Log.e(TAG, "时间格式化失败", e); } } return videos; } private void playVideo(VideoInfo video) { // 停止当前播放 stopPlayback(); // 获取播放地址 playWithTimeRange(video); } private void playWithTimeRange(VideoInfo video) { try { // 创建播放器实例 mEZPlayer = EZOpenSDK.getInstance().createPlayer(mDeviceSerial, mCameraNo); // 设置回调处理器 mEZPlayer.setHandler(mHandler); // 设置验证码 mEZPlayer.setPlayVerifyCode(mVerifyCode); // 关联播放视图 if (mSurfaceHolder != null) { mEZPlayer.setSurfaceHold(mSurfaceHolder); } // 开始播放云存储录像 - 直接使用时间范围 Calendar startCal = Calendar.getInstance(); startCal.setTimeInMillis(video.startTime); Calendar endCal = Calendar.getInstance(); endCal.setTimeInMillis(video.endTime); // 开始播放云存储录像 - 使用Calendar对象 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(); } isVideoPlaying = true; runOnUiThread(() -> { if (mPauseButton != null) { mPauseButton.setImageResource(R.drawable.play_full_pause_sel); } }); } private void setupFullscreenButton() { CheckTextButton fullscreenButton = findViewById(R.id.fullscreen_button); fullscreenButton.setOnClickListener(v -> toggleFullscreen()); // 确保退出按钮在横屏模式下可点击 mExitFullScreenButton.setOnClickListener(v -> toggleFullscreen()); mExitFullScreenButton.setClickable(true); // 明确设置可点击 } private void toggleFullscreen() { if (isFullScreen) { // 退出全屏 setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); // 恢复UI布局 mControlArea.setVisibility(View.VISIBLE); mDisplayLayout.setVisibility(View.VISIBLE); mExitFullScreenButton.setVisibility(View.GONE); mTitleBar.setVisibility(View.VISIBLE); // 恢复播放器布局 RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams( RelativeLayout.LayoutParams.MATCH_PARENT, getResources().getDimensionPixelSize(R.dimen.playback_height) // 300dp ); mPlaybackArea.setLayoutParams(params); } else { // 进入全屏 setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); // 隐藏其他UI元素 mControlArea.setVisibility(View.GONE); mDisplayLayout.setVisibility(View.GONE); mExitFullScreenButton.setVisibility(View.VISIBLE); mTitleBar.setVisibility(View.GONE); // 设置播放器全屏 RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams( RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT ); mPlaybackArea.setLayoutParams(params); // 强制SurfaceView重绘 if (mSurfaceHolder != null) { mSurfaceHolder.setFixedSize(getScreenWidth(), getScreenHeight()); } } isFullScreen = !isFullScreen; } private int getScreenWidth() { return getResources().getDisplayMetrics().widthPixels; } // 获取屏幕高度 private int getScreenHeight() { return getResources().getDisplayMetrics().heightPixels; } @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); // 横屏时隐藏标题栏 if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) { mTitleBar.setVisibility(View.GONE); // 确保播放器填充屏幕 RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams( RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT ); mPlaybackArea.setLayoutParams(params); } else { mTitleBar.setVisibility(View.VISIBLE); // 恢复竖屏布局 RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams( RelativeLayout.LayoutParams.MATCH_PARENT, getResources().getDimensionPixelSize(R.dimen.playback_height) // 300dp ); mPlaybackArea.setLayoutParams(params); } // 确保播放器正确重绘 if (mEZPlayer != null && mSurfaceHolder != null) { mEZPlayer.setSurfaceHold(mSurfaceHolder); } } private void stopPlayback() { if (mEZPlayer != null) { try { mEZPlayer.stopPlayback(); mEZPlayer.release(); } catch (Exception e) { Log.e(TAG, "停止播放失败", e); } mEZPlayer = null; } // 重置播放状态 isVideoPlaying = false; runOnUiThread(() -> { if (mPauseButton != null) { mPauseButton.setImageResource(R.drawable.play_full_pause_sel); } }); } // 播放器回调处理器 - 增强回调处理 private Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case MSG_PLAYBACK_PLAY_SUCCESS: Log.i(TAG, "回放播放成功"); Toast.makeText(FanHui.this, "回放播放成功", Toast.LENGTH_SHORT).show(); isVideoPlaying = true; // 确保状态同步 break; case MSG_PLAYBACK_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无效"; } else if (errorCode == ErrorCode.ERROR_TRANSF_DEVICE_OFFLINE) { errorMsg = "设备离线"; } Toast.makeText(FanHui.this, errorMsg, Toast.LENGTH_LONG).show(); break; case MSG_PLAYBACK_STOP: Log.i(TAG, "回放停止"); isVideoPlaying = false; // 确保状态同步 break; } } }; @Override protected void onPause() { super.onPause(); stopPlayback(); shutdownExecutor(); } @Override protected void onResume() { super.onResume(); // 重新创建线程池 if (mExecutorService == null) { mExecutorService = Executors.newFixedThreadPool(2); } } @Override protected void onDestroy() { super.onDestroy(); stopPlayback(); shutdownExecutor(); } private void shutdownExecutor() { if (mExecutorService != null) { try { mExecutorService.shutdownNow(); if (!mExecutorService.awaitTermination(1, TimeUnit.SECONDS)) { Log.w(TAG, "线程池未能在超时时间内关闭"); } } catch (InterruptedException e) { Log.e(TAG, "线程池关闭被中断", e); Thread.currentThread().interrupt(); } mExecutorService = null; } } // 录像信息数据结构 public static class VideoInfo { String id; long startTime; long endTime; String timeRange; } // 列表适配器 public 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; } } } 用最简单的方式最少的代码修改量实现从MainActivity进入FanHui后,再次返回MainActivity时MainActivity继续播放直播预览视频。

开发文档: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; } } }

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替代TextureView 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); // 系统自动选择 // 发送请求 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; } } } 开发文档:对摄像机存储于SD卡、云端的录像进行取流,查看当前摄像机的历史回放画面。 1. 第一步创建播放器 可调用EZOpenSDK中的 createPlayer 方法创建播放器。 2. 第二步配置播放器 播放器创建完成后需要进行设置代理,设置播放视图,验证码设置等配置。 3. 第三步开始播放 调用startPlayback(EZCloudRecordFile cloudFile) 或 startPlayback(EZDeviceRecordFile deviceFile) 开始回放 4. 第四步结束播放 调用stopPlayback结束回放 5.第五步释放播放器 调用release释放播放器 。根据时间获取存储文件信息 接口功能: 该接口用于根据时间获取存储文件信息。注: 请求地址: 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 。检查FanHui.java代码是不是严格按照开发文档来实现完整的视频回放功能

// 更新API地址为云存储专用接口 private static final String PLAYBACK_URL_API = "https://open.ys7.com/api/lapp/device/video/urls"; private void fetchPlaybackUrl(VideoInfo video) { if (isFinishing() || isDestroyed()) return; mExecutorService.execute(() -> { try { // 构建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("&recType=1"); // 1-云存储录像 postData.append("&videoId=").append(URLEncoder.encode(video.id, "UTF-8")); // 传递录像ID URL url = new URL(PLAYBACK_URL_API); 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); // 发送请求 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)) { JSONObject data = json.getJSONObject("data"); JSONArray urls = data.getJSONArray("urls"); if (urls.length() > 0) { String playbackUrl = urls.getJSONObject(0).getString("url"); runOnUiThread(() -> playWithUrl(playbackUrl, video)); } else { runOnUiThread(() -> Toast.makeText(FanHui.this, "未获取到有效播放地址", Toast.LENGTH_SHORT).show()); } } else { String msg = json.optString("msg", "未知错误"); runOnUiThread(() -> Toast.makeText(FanHui.this, "获取播放地址失败: " + msg, Toast.LENGTH_SHORT).show()); } } else { runOnUiThread(() -> Toast.makeText(FanHui.this, "网络请求失败: " + responseCode, Toast.LENGTH_SHORT).show()); } conn.disconnect(); } catch (Exception e) { runOnUiThread(() -> Toast.makeText(FanHui.this, "获取播放地址出错: " + e.getMessage(), Toast.LENGTH_SHORT).show()); } }); } private void playWithUrl(String url, VideoInfo video) { try { // 创建播放器实例 mEZPlayer = EZOpenSDK.getInstance().createPlayer(mDeviceSerial, mCameraNo); // 设置回调处理器 mEZPlayer.setHandler(mHandler); // 设置验证码 mEZPlayer.setPlayVerifyCode(mVerifyCode); // 关联播放视图 if (mSurfaceHolder != null) { mEZPlayer.setSurfaceHold(mSurfaceHolder); } // 开始播放云存储录像 mEZPlayer.startPlaybackFromCloud(url); Toast.makeText(this, "开始播放录像: " + video.timeRange, Toast.LENGTH_SHORT).show(); } catch (Exception e) { Toast.makeText(this, "播放录像失败: " + e.getMessage(), Toast.LENGTH_SHORT).show(); } } private void fetchVideosByTime(long startTime, long endTime) { if (isFinishing() || isDestroyed()) return; mExecutorService.execute(() -> { try { // 使用V3版本API String urlStr = "https://open.ys7.com/api/lapp/v3/videos/by/time"; 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(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=1"); // 1-云存储录像 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(); 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", "未知错误"); runOnUiThread(() -> Toast.makeText(FanHui.this, "获取录像失败: " + msg, Toast.LENGTH_SHORT).show()); } } conn.disconnect(); } catch (Exception e) { runOnUiThread(() -> Toast.makeText(FanHui.this, "获取录像出错: " + e.getMessage(), Toast.LENGTH_SHORT).show()); } }); } 把上面代码整合进下面的代码中并且生成整合后完整的代码让我直接复制使用 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 int MSG_PLAYBACK_PLAY_SUCCESS = 3001; private static final int MSG_PLAYBACK_PLAY_FAIL = 3002; // 更新API地址 private static final String VIDEO_BY_TIME_URL = "https://open.ys7.com/api/v3/device/local/video/unify/query"; private static final String PLAYBACK_URL_API = "https://open.ys7.com/api/lapp/v2/live/address/get"; private static final int PROTOCOL_EZOPEN = 1; // EZOpen协议 private static final int TYPE_CLOUD_RECORD = 3; // 云存储录像回放 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(); // 使用MainActivity已初始化的SDK实例 - 关键修复点1 EZOpenSDK.getInstance().setAccessToken(mAccessToken); // 默认加载当天的录像 loadVideosForSelectedDate(); } 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, 1); // 默认为通道1 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() / 1000; // 转换为秒级时间戳 cal.set(mSelectedYear, mSelectedMonth, mSelectedDay, 23, 59, 59); long endTime = cal.getTimeInMillis() / 1000; // 转换为秒级时间戳 // 发起网络请求获取录像 fetchVideosByTime(startTime, endTime); } private void fetchVideosByTime(long startTime, long endTime) { // 检查Activity状态 - 关键修复点2 if (isFinishing() || isDestroyed()) return; mExecutorService.execute(() -> { try { // 构建GET请求URL String urlStr = VIDEO_BY_TIME_URL + "?accessToken=" + URLEncoder.encode(mAccessToken, "UTF-8") + "&deviceSerial=" + URLEncoder.encode(mDeviceSerial, "UTF-8") + "&channelNo=" + mCameraNo + "&startTime=" + startTime + "&endTime=" + endTime + "&pageSize=100"; URL url = new URL(urlStr); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("GET"); conn.setRequestProperty("Content-Type", "application/json"); conn.setConnectTimeout(10000); conn.setReadTimeout(10000); // 处理响应 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)) { JSONObject data = json.getJSONObject("data"); JSONArray records = data.getJSONArray("records"); List<VideoInfo> videos = parseVideoData(records); // 检查Activity状态再更新UI - 关键修复点3 if (!isFinishing() && !isDestroyed()) { runOnUiThread(() -> { mVideoList.clear(); mVideoList.addAll(videos); mAdapter.notifyDataSetChanged(); }); } } else { String msg = json.optString("msg", "未知错误"); Log.e(TAG, "获取录像失败: " + msg); if (!isFinishing() && !isDestroyed()) { runOnUiThread(() -> Toast.makeText(FanHui.this, "获取录像失败: " + msg, Toast.LENGTH_SHORT).show()); } } } else { Log.e(TAG, "HTTP错误: " + responseCode); if (!isFinishing() && !isDestroyed()) { runOnUiThread(() -> Toast.makeText(FanHui.this, "网络请求失败: " + responseCode, Toast.LENGTH_SHORT).show()); } } conn.disconnect(); } catch (Exception e) { Log.e(TAG, "获取录像异常", e); if (!isFinishing() && !isDestroyed()) { runOnUiThread(() -> Toast.makeText(FanHui.this, "获取录像出错: " + e.getMessage(), Toast.LENGTH_SHORT).show()); } } }); } private List<VideoInfo> parseVideoData(JSONArray records) throws JSONException { List<VideoInfo> videos = new ArrayList<>(); SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss", Locale.getDefault()); for (int i = 0; i < records.length(); i++) { JSONObject record = records.getJSONObject(i); VideoInfo video = new VideoInfo(); video.id = record.optString("id"); // 转换为毫秒时间戳 video.startTime = record.optLong("startTime") * 1000; video.endTime = record.optLong("endTime") * 1000; video.recType = record.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(); // 获取播放地址 fetchPlaybackUrl(video); } /** * 获取回放地址并播放 */ private void fetchPlaybackUrl(VideoInfo video) { // 检查Activity状态 if (isFinishing() || isDestroyed()) return; mExecutorService.execute(() -> { try { // 构建开始和结束时间字符串 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()); String startTimeStr = sdf.format(new Date(video.startTime)); String stopTimeStr = sdf.format(new Date(video.endTime)); // 构建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("&protocol=").append(PROTOCOL_EZOPEN); postData.append("&type=").append(TYPE_CLOUD_RECORD); // 3-云存储录像回放 postData.append("&startTime=").append(URLEncoder.encode(startTimeStr, "UTF-8")); postData.append("&stopTime=").append(URLEncoder.encode(stopTimeStr, "UTF-8")); postData.append("&quality=1"); // 1-高清 URL url = new URL(PLAYBACK_URL_API); 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); // 发送请求 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)) { JSONObject data = json.getJSONObject("data"); String playbackUrl = data.getString("url"); // 检查Activity状态再播放视频 if (!isFinishing() && !isDestroyed()) { runOnUiThread(() -> playWithUrl(playbackUrl, video)); } } else { String msg = json.optString("msg", "未知错误"); Log.e(TAG, "获取播放地址失败: " + msg); if (!isFinishing() && !isDestroyed()) { runOnUiThread(() -> Toast.makeText(FanHui.this, "获取播放地址失败: " + msg, Toast.LENGTH_SHORT).show()); } } } else { Log.e(TAG, "HTTP错误: " + responseCode); if (!isFinishing() && !isDestroyed()) { runOnUiThread(() -> Toast.makeText(FanHui.this, "网络请求失败: " + responseCode, Toast.LENGTH_SHORT).show()); } } conn.disconnect(); } catch (Exception e) { Log.e(TAG, "获取播放地址异常", e); if (!isFinishing() && !isDestroyed()) { runOnUiThread(() -> Toast.makeText(FanHui.this, "获取播放地址出错: " + e.getMessage(), Toast.LENGTH_SHORT).show()); } } }); } /** * 使用URL播放视频 */ private void playWithUrl(String url, VideoInfo video) { try { // 创建播放器实例 mEZPlayer = EZOpenSDK.getInstance().createPlayer(mDeviceSerial, mCameraNo); // 设置回调处理器 mEZPlayer.setHandler(mHandler); // 设置验证码 mEZPlayer.setPlayVerifyCode(mVerifyCode); // 关联播放视图 if (mSurfaceHolder != null) { mEZPlayer.setSurfaceHold(mSurfaceHolder); } else { Log.e(TAG, "无法关联播放视图"); Toast.makeText(this, "播放视图未初始化", Toast.LENGTH_SHORT).show(); return; } 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 MSG_PLAYBACK_PLAY_SUCCESS: Log.i(TAG, "回放播放成功"); break; case MSG_PLAYBACK_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 onPause() { super.onPause(); // 关键修复点4:优化线程池管理 if (mExecutorService != null) { mExecutorService.shutdownNow(); mExecutorService = null; } // 停止播放 stopPlayback(); } @Override protected void onResume() { super.onResume(); // 重新创建线程池 if (mExecutorService == null) { mExecutorService = Executors.newFixedThreadPool(2); } } @Override protected void onDestroy() { super.onDestroy(); stopPlayback(); // 关键修复点5:使用shutdownNow立即终止线程 if (mExecutorService != null) { mExecutorService.shutdownNow(); } } // 录像信息数据结构 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; } } }

package 网络.核心类; import java.net.*; import java.io.*; import java.util.Scanner; public class Client { public static void main(String[] args) { try (Socket socket = new Socket("localhost", 12345); // 连接到服务器的IP和端口 PrintWriter out = new PrintWriter(socket.getOutputStream(), true); // 用于发送数据 BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream())); // 用于接收数据 Scanner scanner = new Scanner(System.in)) { // 用于读取键盘输入 System.out.println("Connected to server. Type your message and press enter to send. Type 'exit' to quit."); while (true) { System.out.print("Client: "); String userInput = scanner.nextLine(); // 读取用户输入 if (userInput.equalsIgnoreCase("exit")) { System.out.println("Exiting client..."); break; // 退出循环 } // TODO: 扩展用户输入处理,支持不同命令(如 "echo <消息>"、"time"、"quit")。例如,提示用户可用命令并根据输入类型发送适当的消息。 // 提示:您可以在这里添加代码来解析用户输入,并修改发送逻辑。 out.println(userInput); // 通过Socket发送消息到服务器 String response = in.readLine(); // 通过Socket读取服务器响应 System.out.println("Server response: " + response); // 显示响应 // TODO: 扩展响应处理,根据服务器返回的不同响应类型进行处理(如显示时间或错误消息)。确保处理 null 或异常情况。 } } catch (IOException e) { System.err.println("Connection error: " + e.getMessage()); // 处理连接异常 } } } package 网络.核心类; import java.net.*; import java.io.*; public class Server { public static void main(String[] args) { try (ServerSocket serverSocket = new ServerSocket(12345)) { // 绑定到端口12345 System.out.println("Server started. Listening on port 12345..."); while (true) { // 无限循环,等待客户端连接 Socket clientSocket = serverSocket.accept(); // 接受客户端连接,返回一个Socket对象 System.out.println("Client connected: " + clientSocket.getInetAddress()); // 使用Socket处理客户端通信(这里使用独立的线程处理每个客户端,避免阻塞) new Thread(() -> { try (BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream())); PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)) { String message; while ((message = in.readLine()) != null) { // 读取客户端发送的消息 System.out.println("Received from client: " + message); // TODO: 扩展消息处理逻辑,支持简单协议。例如,根据消息类型执行操作: // - 如果消息以 "echo" 开头,返回回显消息。 // - 如果消息是 "time",返回当前系统时间。 // - 如果消息是 "quit",断开连接。 // 提示:使用 if-else 语句或 switch 表达式来处理不同命令。确保处理无效命令,返回错误消息。 out.println("Echo: " + message); // 默认回显逻辑,学生需要修改此部分 } } catch (IOException e) { System.err.println("Error handling client: " + e.getMessage()); } finally { try { clientSocket.close(); // 关闭Socket } catch (IOException e) { e.printStackTrace(); } } }).start(); // 启动新线程处理客户端 } } catch (IOException e) { System.err.println("Server error: " + e.getMessage()); // 处理异常,如端口被占用 } } }补充完善上述代码

最新推荐

recommend-type

【大学生电子设计】:备战2015全国大学生电子设计竞赛-信号源类赛题分析.pdf

【大学生电子设计】:备战2015全国大学生电子设计竞赛-信号源类赛题分析.pdf
recommend-type

湘潭大学人工智能专业2024级大一C语言期末考试题库项目-包含58个从头歌平台抓取并排版的C语言编程题目及解答-涵盖基础语法-数组操作-条件判断-循环结构-函数调用-指针应用等核心.zip

2025电赛预测湘潭大学人工智能专业2024级大一C语言期末考试题库项目_包含58个从头歌平台抓取并排版的C语言编程题目及解答_涵盖基础语法_数组操作_条件判断_循环结构_函数调用_指针应用等核心.zip
recommend-type

基于STM32的步进电机S型曲线与SpTA算法高效控制及其实现 T梯形算法 经典版

基于STM32平台的步进电机S型曲线和SpTA加减速控制算法。首先阐述了S型曲线控制算法的特点及其在STM32平台上的实现方法,包括设置启动频率、加速时间和最高速度等参数,以减少电机启动和停止时的冲击。接着讨论了T梯形算法与S型曲线的结合,通过精确的时间控制提高运动精度。然后重点讲解了SpTA算法,强调其自适应性强、不依赖PWM定时器个数,能够根据实际运行状态动态调整。文中提到一种高效的非DMA数据传输方式,提升了CPU效率并解决了外部中断时无法获取已输出PWM波形个数的问题。最后介绍了SpTA算法在多路电机控制方面的优势。 适合人群:从事嵌入式系统开发、自动化控制领域的工程师和技术人员,尤其是对步进电机控制有研究兴趣的人群。 使用场景及目标:适用于需要高精度和平滑控制的步进电机应用场合,如工业机器人、数控机床等领域。目标是帮助开发者掌握STM32平台下步进电机的先进控制算法,优化系统的实时性能和多电机协同控制能力。 其他说明:文章提供了详细的理论背景和技术细节,有助于读者深入理解并应用于实际项目中。
recommend-type

一个面向Android开发者的全面知识体系整理项目-包含Android基础-Framework解析-系统启动流程-四大组件原理-Handler-Binder-AMS-WMS等核心机.zip

python一个面向Android开发者的全面知识体系整理项目_包含Android基础_Framework解析_系统启动流程_四大组件原理_Handler_Binder_AMS_WMS等核心机.zip
recommend-type

日前日内两阶段调度综合能源分析:基于Matlab与Yalmip的优化结果对比及应用

日前日内两阶段调度在综合能源管理中的应用,特别是利用Matlab和Yalmip工具箱进行优化建模的方法。文章分析了三种不同调度场景(日前不考虑需求响应调度、日前考虑需求响应调度、日前日内两阶段调度),并比较了各自的优化结果。重点讨论了机组成本和弃风惩罚两个关键指标,展示了两阶段调度在提高电力供应稳定性和降低运营成本方面的优势。 适合人群:从事能源管理和电力系统调度的研究人员和技术人员,尤其是对优化算法和Matlab编程有一定基础的人群。 使用场景及目标:①理解和掌握日前日内两阶段调度的基本概念及其应用场景;②学习如何使用Matlab和Yalmip工具箱进行优化建模;③评估不同调度策略对机组成本和弃风惩罚的影响。 其他说明:本文不仅提供了详细的理论解释,还有具体的编程实例和优化结果对比,有助于读者深入理解调度优化的实际操作和效果。
recommend-type

Pansophica开源项目:智能Web搜索代理的探索

Pansophica开源项目是一个相对较新且具有创新性的智能Web搜索代理,它突破了传统搜索引擎的界限,提供了一种全新的交互方式。首先,我们来探讨“智能Web搜索代理”这一概念。智能Web搜索代理是一个软件程序或服务,它可以根据用户的查询自动执行Web搜索,并尝试根据用户的兴趣、历史搜索记录或其他输入来提供个性化的搜索结果。 Pansophica所代表的不仅仅是搜索结果的展示,它还强调了一个交互式的体验,在动态和交互式虚拟现实中呈现搜索结果。这种呈现方式与现有的搜索体验有着根本的不同。目前的搜索引擎,如Google、Bing和Baidu等,多以静态文本和链接列表的形式展示结果。而Pansophica通过提供一个虚拟现实环境,使得搜索者可以“扭转”视角,进行“飞行”探索,以及“弹网”来浏览不同的内容。这种多维度的交互方式使得信息的浏览变得更加快速和直观,有望改变用户与网络信息互动的方式。 接着,我们关注Pansophica的“开源”属性。所谓开源,指的是软件的源代码可以被公众获取,任何个人或组织都可以自由地使用、学习、修改和分发这些代码。开源软件通常由社区进行开发和维护,这样的模式鼓励了协作创新并减少了重复性劳动,因为全世界的开发者都可以贡献自己的力量。Pansophica项目作为开源软件,意味着其他开发者可以访问和使用其源代码,进一步改进和扩展其功能,甚至可以为Pansophica构建新的应用或服务。 最后,文件名称“Pansophica-src-1.3”表明了我们讨论的特定版本的Pansophica开源代码。数字“1.3”很可能指的是该版本号,表明这是Pansophica项目的第1.3个公开版本。这个版本号对于开发者来说很重要,因为它可以帮助他们追踪更新的进度、比较版本间的差异以及选择最适合其项目的版本。 综合以上信息,Pansophica开源项目不仅在搜索代理领域引入了创新的交互方式,而且通过开源的方式,提供了技术共享和协作的可能性。对于IT行业来说,开源项目是推动技术发展的重要力量,它激发了社区的活力,促进了知识的传播和技术的进步。通过构建一个开源社区,Pansophica可以吸引来自世界各地的开发者和研究人员,共同探索如何将搜索技术与交互式虚拟现实技术结合起来,为用户带来更加丰富和有效的信息检索体验。同时,Pansophica的开源性质也使其更容易适应未来技术的发展,随着社区的不断贡献和反馈,这一平台有望在功能和性能上得到持续的优化和增强。
recommend-type

跨平台内容提取无忧:coze工作流应对社交媒体挑战

# 1. 社交媒体内容提取的必要性与挑战 ## 1.1 内容提取的必要性 在数字时代,社交媒体内容是信息与数据流动的主干道,公司、个人和研究者都在寻求从这些信息中提取价值。无论是为了市场趋势分析、品牌监控、消费者洞察还是公共舆论监测,有效的社交媒体内容提取变得至关重要。这不仅有助于企业做出数据驱动的决策,也能使个人更好地理
recommend-type

vrrp主设备发送的免费arp

<think>好的,我现在需要回答用户关于VRRP主设备发送免费ARP的目的和工作原理的问题。首先,我得回忆一下VRRP的基本概念和工作机制。VRRP,虚拟路由冗余协议,主要用于在局域网中提供默认网关的冗余备份,确保网络的高可用性。主设备(Master)负责转发流量,而备用设备(Backup)则在主设备故障时接管工作。 用户的问题集中在主设备发送免费ARP的目的和机制上。根据引用[2],免费ARP用于通知下游设备虚拟MAC地址的变更。当主设备被选举出来后,它需要让局域网内的其他设备知道虚拟IP对应的MAC地址已经指向自己,这样流量才会被正确路由到主设备。免费ARP的作用应该就是更新这些设备的
recommend-type

为Ghost博客平台打造的Meteor流星包装使用指南

从给定文件信息中,我们可以提炼出以下IT知识点: ### 标题知识点:流星Ghost软件包 1. **流星Ghost软件包的用途**:流星Ghost软件包是专为Ghost博客平台设计的流星(Meteor)应用程序。流星是一个开源的全栈JavaScript平台,用于开发高性能和易于编写的Web应用程序。Ghost是一个开源博客平台,它提供了一个简单且专业的写作环境。 2. **软件包的作用**:流星Ghost软件包允许用户在流星平台上轻松集成Ghost博客。这样做的好处是可以利用流星的实时特性以及易于开发和部署的应用程序框架,同时还能享受到Ghost博客系统的便利和美观。 ### 描述知识点:流星Ghost软件包的使用方法 1. **软件包安装方式**:用户可以通过流星的命令行工具添加名为`mrt:ghost`的软件包。`mrt`是流星的一个命令行工具,用于添加、管理以及配置软件包。 2. **初始化Ghost服务器**:描述中提供了如何在服务器启动时运行Ghost的基本代码示例。这段代码使用了JavaScript的Promise异步操作,`ghost().then(function (ghostServer) {...})`这行代码表示当Ghost服务器初始化完成后,会在Promise的回调函数中提供一个Ghost服务器实例。 3. **配置Ghost博客**:在`then`方法中,首先会获取到Ghost服务器的配置对象`config`,用户可以在此处进行自定义设置,例如修改主题、配置等。 4. **启动Ghost服务器**:在配置完成之后,通过调用`ghostServer.start()`来启动Ghost服务,使其能够处理博客相关的请求。 5. **Web浏览器导航**:一旦流星服务器启动并运行,用户便可以通过Web浏览器访问Ghost博客平台。 ### 标签知识点:JavaScript 1. **JavaScript作为流星Ghost软件包的开发语言**:标签指出流星Ghost软件包是使用JavaScript语言开发的。JavaScript是一种在浏览器端广泛使用的脚本语言,它也是流星平台的基础编程语言。 2. **流星和Ghost共同使用的语言**:JavaScript同样也是Ghost博客平台的开发语言。这表明流星Ghost软件包可以无缝集成,因为底层技术栈相同。 ### 压缩包子文件的文件名称列表知识点:meteor-ghost-master 1. **版本控制和软件包结构**:文件名称`meteor-ghost-master`暗示了该软件包可能托管在像GitHub这样的版本控制系统上。文件名中的`master`通常指的是主分支或主版本。 2. **软件包的目录结构**:通过文件名称可以推断出该软件包可能拥有一个标准的流星软件包结构,包含了初始化、配置、运行等必要的模块和文件。 3. **软件包的维护状态**:由于文件名没有包含特定的版本号,我们无法直接得知软件包的最新更新情况。通常,软件包维护者会将最新的版本代码放在`master`分支上。 ### 总结 流星Ghost软件包提供了一个有效的解决方案,使得流星平台的开发者能够在他们的应用中添加Ghost博客功能。软件包的使用简便,通过流星的命令行工具安装,并通过JavaScript代码配置和启动Ghost服务。通过流星Ghost软件包,开发者能够享受流星的实时特性以及Ghost博客系统的便利性。此外,软件包的命名和结构也暗示了其维护和版本控制的模式,有助于开发者更好地理解如何使用和维护这一软件包。
recommend-type

抖音标题生成自动化:用coze工作流释放创意

# 1. 抖音标题生成自动化的重要性 随着社交媒体平台的崛起,内容的吸引力很大程度上取决于标题的创意与精准性。抖音作为一个日活亿级的短视频平台,高质量的标题能够有效提高视频的点击率,增加内容的传播。但是,人工撰写标题不仅耗时耗力,而且很难做到快速响应热点,自动化标题生成工具应运而生。coze工作流,作为一种实现自动化生成抖音标题的工具,其重要性不言而喻。它能够利用大数据分析和机器学习技术,提高标题的吸引