Android核心技术-day05-05-安卓下的多线程下载(带ProgressBar)有bug版

本文介绍了一种基于Android平台的多线程文件下载方法,使用了xUtils库进行网络请求,通过创建多个线程并分配不同的下载范围,实现了文件的并发下载。文章详细解析了如何设置线程数量、分配下载范围、处理断点续传以及更新进度条等功能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

package com.gaozewen.multidownloader;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.Toast;

import com.lidroid.xutils.HttpUtils;
import com.lidroid.xutils.exception.HttpException;
import com.lidroid.xutils.http.ResponseInfo;
import com.lidroid.xutils.http.callback.RequestCallBack;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;

public class MainActivity extends AppCompatActivity {

    private EditText mEt_thread_count;
    private EditText mEt_path;
    private LinearLayout mLl_container;

    public String path = "http://192.168.1.102:8080/xunlei.exe";
    public int totalThreadCount = 0;
    public int runningThreadCount = 0;
    private ArrayList<ProgressBar> mPbs;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 1. 找到控件
        // 2. 在 button 点击事件中下载文件
        // 3. 下载的时候显示下载进度
        mEt_thread_count = (EditText) findViewById(R.id.et_thread_count);
        mEt_path = (EditText) findViewById(R.id.et_path);
        mLl_container = (LinearLayout) findViewById(R.id.ll_container);
    }

    /**
     * 用第三方控件 xUtils 下载
     * @param view
     */
    public void download2(View view) {
        path =  mEt_path.getText().toString().trim();
        HttpUtils http = new HttpUtils();
        http.download(path, getCacheDir() + "/" + getDownloadFileName(path), true, new RequestCallBack<File>() {
            @Override
            public void onSuccess(ResponseInfo<File> responseInfo) {
                Toast.makeText(MainActivity.this,"下载成功", Toast.LENGTH_SHORT).show();
            }

            @Override
            public void onFailure(HttpException e, String s) {
                Toast.makeText(MainActivity.this,"下载失败", Toast.LENGTH_SHORT).show();
            }

            @Override
            public void onLoading(long total, long current, boolean isUploading) {
                System.out.println(current);
                super.onLoading(total, current, isUploading);
            }
        });
    }

    public void download(View view) {
        totalThreadCount = Integer.valueOf(mEt_thread_count.getText().toString().trim());
        path = mEt_path.getText().toString().trim();
        mLl_container.removeAllViews();

        mPbs = new ArrayList<>();
        for (int i = 0; i < totalThreadCount; i++) {
            // 有几个线程就创建几个 进度条
            ProgressBar pb = (ProgressBar) View.inflate(this, R.layout.pb, null);
            mLl_container.addView(pb);
            mPbs.add(pb);
        }

        new Thread() {
            @Override
            public void run() {
                try {
                    URL url = new URL(path);
                    HttpURLConnection conn = (HttpURLConnection) url.openConnection();
                    conn.setRequestMethod("GET");
                    int code = conn.getResponseCode();
                    if (code == 200) {
                        int length = conn.getContentLength();
                        System.out.println("file length: " + length);
                        // 创建一个空的文件,并且设置他的文件长度等于服务器上的文件长度
                        RandomAccessFile raf = new RandomAccessFile(getCacheDir() + "/" + getDownloadFileName(path), "rw");
                        raf.setLength(length);
                        raf.close();

                        int blockSize = length / totalThreadCount;
                        System.out.println("every block size: " + blockSize);
                        runningThreadCount = totalThreadCount;
                        for (int threadId = 0; threadId < totalThreadCount; threadId++) {
                            int startPosition = blockSize * threadId;
                            int endPosition = blockSize * (threadId + 1) - 1; // -1 是到当前块的最后一个字节
                            if (threadId == (totalThreadCount - 1)) {
                                endPosition = length - 1;
                            }
                            System.out.println(String.format("threadId: %s\ndownload rage: %s to %s", threadId, startPosition, endPosition));

                            new DownloadThread(threadId, startPosition, endPosition).start();
                        }
                    } else {

                    }

                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }.start();
    }

    /**
     * 从网路路径获取文件名
     *
     * @param path 网络路径
     * @return 文件名
     */
    private static String getDownloadFileName(String path) {
        return path.substring(path.lastIndexOf("/") + 1);
    }

    /**
     * 下载文件的线程
     */
    private class DownloadThread extends Thread {
        /**
         * 线程 id
         */
        private int threadId;
        /**
         * 当前线程下载的起始位置
         */
        private int startPosition;
        /**
         * 当前线程下载的终止位置
         */
        private int endPosition;

        /**
         * 当前线程需要去下载的总共的字节
         */
        private int threadTotal;

        /**
         * 上次断点 下载的总共字节数
         */
        private int lastDownloadTotalSize;

        public DownloadThread(int threadId, int startPosition, int endPosition) {
            this.threadId = threadId;
            this.startPosition = startPosition;
            this.endPosition = endPosition;
            this.threadTotal = endPosition - startPosition;

            mPbs.get(threadId).setMax(threadTotal); // 设置进度条
        }

        @Override
        public void run() {
            System.out.println(String.format("threadID: %s  begin working", threadId));
            // lest thread download it's self range data
            try {
                // 查询 记录断点信息的文件
                File finfo = new File(getCacheDir(), totalThreadCount + getDownloadFileName(path) + threadId + ".txt");
                if (finfo.exists() && finfo.length() > 0) { // 断点下载
                    FileInputStream fis = new FileInputStream(finfo);
                    BufferedReader br = new BufferedReader(new InputStreamReader(fis));
                    String lastPosition = br.readLine();
                    int intLastPosition = Integer.parseInt(lastPosition);

                    // 进度条断点续借位置
                    lastDownloadTotalSize = intLastPosition - startPosition;

                    // This thread download data before times;
                    startPosition = intLastPosition;
                    fis.close();
                }
                // 第一次下载
                URL url = new URL(path);
                HttpURLConnection conn = (HttpURLConnection) url.openConnection();
                conn.setRequestMethod("GET");
                System.out.println(String.format("begin and end: %s  range of download: %s to %s", threadId, startPosition, endPosition));
                conn.setRequestProperty("Range", String.format("bytes=%s-%s", startPosition, endPosition));
                // 从服务器下载资源
                int code = conn.getResponseCode();
                if (code == 206) {
                    InputStream is = conn.getInputStream();
                    // 第一次没有这个文件则创建,下次直接打开这个文件
                    RandomAccessFile raf = new RandomAccessFile(getCacheDir() + "/" + getDownloadFileName(path), "rw");

                    // !!! position of begin to write
                    raf.seek(startPosition); // 将指针移动到这次开始续传的位置

                    byte[] buffer = new byte[1024];
                    int len = -1;
                    int total = 0; // 当前线程 这次下载的数据
                    while ((len = is.read(buffer)) != -1) {
                        raf.write(buffer, 0, len);
                        total += len;
                        // 创建记录 保存当前线程下载的位置 (注意: rwd 同步实时写入到设备中)
                        RandomAccessFile infoRaf = new RandomAccessFile(getCacheDir() + "/" + totalThreadCount + getDownloadFileName(path) + threadId + ".txt", "rwd");
                        infoRaf.write(String.valueOf(startPosition + total).getBytes());
                        infoRaf.close();

                        // 每次读写显示一个进度
                        mPbs.get(threadId).setProgress(total + lastDownloadTotalSize);
                    }
                    raf.close();
                    is.close();
                    System.out.println(String.format("thread: %s  download complete...", threadId));
                } else {
                    System.out.println("request download failed");
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                synchronized (MainActivity.class) {
                    runningThreadCount--;
                    if (runningThreadCount <= 0) { // 多线程下载完成 删除 位置信息文件
                        System.out.println("multi thread download complete.");
                        for (int i = 0; i < totalThreadCount; i++) {
                            File positionFile = new File(getCacheDir(),totalThreadCount + getDownloadFileName(path) + i + ".txt");
//                            System.out.println(positionFile.delete());
                        }
                    }
                }
            }
        }
    }

}

 

开发文档: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; } } }
最新发布
06-26
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值