//
//  EZPlaybackViewController.m
//  EZOpenSDKDemo
//
//  Created by DeJohn Dong on 15/11/3.
//  Copyright © 2015年 Ezviz. All rights reserved.
//

#import "EZPlaybackViewController.h"
#import <Photos/Photos.h>
#import "FCFileManager.h"
#import "EZBusinessTool.h"
#import "EZStreamConfigLoader.h"
#import "EZCommonTool.h"
#import "EZScreenOrientationTool.h"
#import "UIViewController+EZBackPop.h"
#import "UIButton+EZNav.h"
#import "UIAlertController+DeviceVerifyCodeBySerial.h"

#import "EZDeviceInfo.h"
#import "EZCameraInfo.h"
#import "EZSubDeviceInfo.h"
#import "EZRecordDownloader.h"
#import "EZDeviceRecordDownloadTask.h"
#import "EZCloudRecordDownloadTask.h"
#import "EZDeviceRecordFile.h"
#import "EZCloudRecordFile.h"
#import "EZDeviceRecordInfo.h"
#import "EZVideoTransformer.h"
#import "EZRecordCoverFetcherManager.h"

#import "EZPlayer.h"
#import "EZAICloudStorageView.h"
#import "MJRefresh.h"
#import "FTPopOverMenu.h"
#import "MBProgressHUD.h"
#import "HIKLoadView.h"
#import "DDCollectionViewFlowLayout.h"
#import "EZRecordCell.h"
#import "FecViewLayoutHelper.h"

#import "EZAbroadCloudServiceExVC.h"

#define MinimumZoomScale 1.0
#define MaximumZoomScale 4.0

@interface EZPlaybackViewController ()<UICollectionViewDataSource, DDCollectionViewDelegateFlowLayout, EZPlayerDelegate, RecordCoverFetcherDelegate, EZAICloudStorageViewDelegate> {
    EZVideoRecordType recordType;///< 录像类型，仅仅支持国内
    
    BOOL _isOpenSound;///< 是否打开声音
    BOOL _isPlayingWhenJump;///< 此页面跳转到其他页面时，播放器是否在播放
    BOOL _isCloudRightNavHidden;///< 云存储右上角导航是否隐藏(海外显示，国内隐藏)
    
    EZPlaybackStatus _playbackStatus;///< 回放播放状态
    NSTimeInterval _playSeconds;///< 播放秒数
    NSTimeInterval _duringSeconds;///< 录像时长
    
    EZDeviceRecordFile *_deviceRecord;///< 点击选中的SD卡视频
    EZCloudRecordFile *_cloudRecord;///< 点击选中的云存储视频
    
    BOOL _isShowToolbox;///< 播放器工具栏是否显示
}

@property (nonatomic, strong) EZPlayer *player;
@property (nonatomic, strong) HIKLoadView *loadingView;///< 播放加载进程UI
@property (nonatomic, strong) UIButton *recordTypeButton;///< SD卡视频类型选择
@property (weak, nonatomic) IBOutlet UIView *playerView;///< 播放视图
@property (weak, nonatomic) IBOutlet UIView *playerViewDividLine;///< 两个播放窗口间的黑色分割线
@property (weak, nonatomic) IBOutlet UIView *secondPlayerView;///< 双目设备第二个镜头播放视图
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *secondPlayerViewTopMarginContraint;///< 与第一个PlayerView的间距
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *playerViewAspectContraint;
@property (weak, nonatomic) IBOutlet UILabel *streamTypeLabel;///< 回放取流方式
@property (weak, nonatomic) IBOutlet UILabel *messageLabel;///< 信息Label
@property (weak, nonatomic) IBOutlet UILabel *largeTitleLabel;///< 标题
@property (weak, nonatomic) IBOutlet UIDatePicker *datePicker;///< 导航栏日期选择器
@property (weak, nonatomic) IBOutlet UITextField *dateTextField;///< 日期选择
@property (weak, nonatomic) IBOutlet UIToolbar *dateToolbar;///< 日期选中工具栏(取消&确定)
@property (weak, nonatomic) IBOutlet UIButton *dateButton;

// 播放进度条区域 + 播放工具栏区域
@property (weak, nonatomic) IBOutlet UIView *playerToolbox;///< 播放进度条区域 + 播放工具栏区域
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *playerToolboxTopConstraint;///< 播放工具栏顶部约束，与第一个播放器窗口底部距离
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *playerToolboxHeightConstraint;///< 播放工具栏高度约束
// 播放进度条区域
@property (weak, nonatomic) IBOutlet UIView *sliderAreaView;///< 播放进度条区域
@property (weak, nonatomic) IBOutlet UILabel *playTimeLabel;///< 播放进度条左侧，录像已播放时长
@property (weak, nonatomic) IBOutlet UILabel *duringTimeLabel;///< 播放进度条右侧，录像总时长
@property (weak, nonatomic) IBOutlet UISlider *duringSlider;///< 播放进度条
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *sliderLeftConstraint;///< 进度条左侧约束
// 播放工具栏区域
@property (weak, nonatomic) IBOutlet UIView *controlBarView;///< 工具栏父视图
@property (weak, nonatomic) IBOutlet UIButton *playButton;///< 播放/暂停按钮
@property (weak, nonatomic) IBOutlet UIButton *voiceButton;///< 声音开关按钮
@property (weak, nonatomic) IBOutlet UIButton *downloadButton;///< 下载按钮
@property (weak, nonatomic) IBOutlet UIButton *rateBtn;///< 倍数设置按钮
@property (weak, nonatomic) IBOutlet UIButton *aiCloudBtn;///< AI云存储按钮
@property (weak, nonatomic) IBOutlet UIButton *largeButton;///< 全屏按钮
@property (weak, nonatomic) IBOutlet UIButton *largeBackButton;///< 全屏返回按钮

@property (weak, nonatomic) IBOutlet UIImageView *cloudAdImage;///< 云存储广告图片
@property (weak, nonatomic) IBOutlet UIImageView *noVideoImageView;///< 无录像图片
@property (weak, nonatomic) IBOutlet UILabel *noVideoLabel;///< 无录像文本
@property (weak, nonatomic) IBOutlet UICollectionView *playbackList;///< 录像列表
@property (weak, nonatomic) IBOutlet UIView *toolView;///< 云存储/SD卡录像切换父视图
@property (weak, nonatomic) IBOutlet UIButton *cloudButton;///< 云存储Tab按钮
@property (weak, nonatomic) IBOutlet UIButton *sdkCloudRecordButton;///< SDK云录制Tab按钮
@property (weak, nonatomic) IBOutlet UIButton *deviceButton;///< SD卡录像Tab按钮
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *selectedImageViewConstraint;///< 云存储/SD卡录像选中状态下的蓝色线条约束

@property (weak, nonatomic) IBOutlet UIView *fecView;///< 鱼眼设备模式矫正视图
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *fecViewContraint;///< 鱼眼矫正顶部距离
@property (weak, nonatomic) IBOutlet UIButton *placeWallBtn;///< 壁装
@property (weak, nonatomic) IBOutlet UIButton *placeFloorBtn;///< 底装
@property (weak, nonatomic) IBOutlet UIButton *placeCeilingBtn;///< 顶装
@property (weak, nonatomic) IBOutlet UIButton *correct4PtzBtn;///< 4分屏
@property (weak, nonatomic) IBOutlet UIButton *correct5PtzBtn;///< 5分屏
@property (weak, nonatomic) IBOutlet UIButton *correctFull5PtzBtn;///< 全景5分屏
@property (weak, nonatomic) IBOutlet UIButton *correctLatBtn;///< 维度拉伸
@property (weak, nonatomic) IBOutlet UIButton *correctARCHorBtn;///< ARC水平
@property (weak, nonatomic) IBOutlet UIButton *correctARCVerBtn;///< ARC垂直
@property (weak, nonatomic) IBOutlet UIButton *correctWideAngleBtn;///< 广角
@property (weak, nonatomic) IBOutlet UIButton *correct180Btn;///< 180°全景
@property (weak, nonatomic) IBOutlet UIButton *correct360Btn;///< 360°全景
@property (weak, nonatomic) IBOutlet UIButton *correctCycBtn;///< 柱状
@property (nonatomic, strong) NSArray<UIButton *> *fecCorrectTypeButtons;

@property (nonatomic, strong) NSURLSessionDataTask *operation;///< 录像下载管理器
@property (nonatomic, strong) NSTimer *playbackTimer;///< 回放过程中刷新当前播放描述播放进程
@property (nonatomic, assign) EZRecordCategory recordCategory;///< 当前选中的录像类别
@property (nonatomic, assign) BOOL selectedCell;///< 是否有选中某一个cell
@property (nonatomic, strong) NSIndexPath *selectedIndexPath;///< 当前选中的录像所在的indexPath

@property (nonatomic, strong) NSDate *beginTime;///< 录像查询开始时间
@property (nonatomic, strong) NSDate *endTime;///< 录像查询结束时间
@property (nonatomic, strong) NSMutableArray *records;///< 录像数组
@property (nonatomic, strong) NSArray<FTPopOverMenuModel *> *recordTypeMenuArray;///< SD卡视频类型数组
@property (nonatomic, strong) EZCameraInfo *cameraInfo;///< 设备通道信息对象
@property (nonatomic, assign) BOOL isMultiChannelDevice;///< 是否是双目设备，如果接入的设备没有双目设备，可以忽略相关逻辑
@property (nonatomic, copy) NSString *verifyCode;///< 设备验证码

@property (nonatomic, strong) FecViewLayoutHelper *fecViewLayoutHelper;///< 鱼眼设备矫正模式视图调整辅助类
@property (nonatomic, assign) EZFecPlaceType fecPlaceType;///< 鱼眼安装模式
@property (nonatomic, assign) EZFecCorrectType fecCorrectType;///< 鱼眼矫正模式

// 电子放大
@property (nonatomic, assign) CGRect rect;
@property (nonatomic, assign) CGRect *zoomRect;///< 显示区域指针
@property (nonatomic, assign) CGFloat lastScale;///< 上一次缩放后的比例
@property (nonatomic, assign) CGPoint movePoint;///< 滑动触碰坐标
// 电子放大
@property (nonatomic, assign) CGRect rect2;
@property (nonatomic, assign) CGRect *zoomRect2;///< 显示区域指针
@property (nonatomic, assign) CGFloat lastScale2;///< 上一次缩放后的比例
@property (nonatomic, assign) CGPoint movePoint2;///< 滑动触碰坐标

@property (weak, nonatomic) IBOutlet UIView *mAICloudStorageContainer;///< AI云存储UI容器
@property (strong, nonatomic) IBOutlet NSLayoutConstraint *mAICloudStorageBottomConstraint;///< AI云存储容器底部约束
@property (nonatomic, strong) EZAICloudStorageView *mAICloudStorageView;///< AI云存储视图管理器
@property (nonatomic, assign) BOOL mIsAICloudStoragePlayback;///< 当前播放是否由AI云存储触发
@property (nonatomic, assign) BOOL mAICloudStorageShowingBeforeRotate;///< 横屏前AI云存储是否在显示

@end

@implementation EZPlaybackViewController

- (instancetype)init {
    if (self = [super init]) {
        self = [EZStoryBoardTool getViewController:@"EZMain" andIdentifier:@"EZPlaybackViewController"];
    }
    return self;
}

#pragma mark - Life Cycle | 生命周期

- (void)viewDidLoad {
    [super viewDidLoad];
    [self setData];// 数据初始化
    [self setUI];// 视图初始化设置，addHeaderRefresh方法里请求录像列表并开始播放
    [self setEZPlayer];// 播放器初始化
    // SD卡本地录像获取初始化
#ifdef EZVIZ_OPEN_DEMO
    // 国内支持SD卡录像封面获取，海外不支持
    // 与设备建立链接，获取SD卡录像封面（页面退出的时候必须断开链接，释放资源，见-dealloc方法）
    [[EZRecordCoverFetcherManager sharedInstance] initFetcherWithDeviceSerial:_cameraInfo.deviceSerial cameraNo:_cameraInfo.cameraNo];
    [EZRecordCoverFetcherManager sharedInstance].fetcherDelegate = self;
    if (![EZBusinessTool isSupportSdCover:self.deviceInfo cameraInfo:_cameraInfo]) {
        [EZToast show:NSLocalizedString(@"device_not_support_sd_record_cover", @"该设备不支持SD卡录像封面")];
    }
#endif
    
    /// 鱼眼设备专用设置，如果没有鱼眼设备，不需要如下代码
    [self setFecUI];
    /// 鱼眼设备专用设置，如果没有鱼眼设备，不需要如上代码
    
#ifdef JCTest
    // 更新进度条左侧约束(测试用，开发者不用看)
    [self.sliderAreaView removeConstraint:self.sliderLeftConstraint];
    NSLayoutConstraint *leftConstraint = [NSLayoutConstraint constraintWithItem:self.duringSlider attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:self.playTimeLabel attribute:NSLayoutAttributeTrailing multiplier:1.0 constant:3.5];
    [self.sliderAreaView addConstraint:leftConstraint];
    self.playTimeLabel.hidden = NO;
#endif
}

- (void)viewWillAppear:(BOOL)animated {
    if (_isPlayingWhenJump) {
        [self startPlaybackByRecordCategory];
        _isPlayingWhenJump = NO;
    }
}

- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
    // performSelector定时方法未执行完就退出的话会导致内存泄漏，退出前取消定时任务
    [NSObject cancelPreviousPerformRequestsWithTarget:self];
    [self invalidateTimer];
    [self.loadingView stopSquareClockwiseAnimation];
    [_player closeSound];
    [_player stopPlayback];
}

- (void)dealloc {
    NSLog(@"%@ dealloc", self.class);
    [EZOPENSDK releasePlayer:_player];
#ifdef EZVIZ_OPEN_DEMO
    [[EZRecordCoverFetcherManager sharedInstance] stopFetcher];// 断开与设备的链接
#endif
}

- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
    return UIInterfaceOrientationMaskAllButUpsideDown;
}

/** 横竖屏切换 */
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
    BOOL isLandscape = toInterfaceOrientation == UIInterfaceOrientationLandscapeLeft ||
                       toInterfaceOrientation == UIInterfaceOrientationLandscapeRight;
    self.navigationController.navigationBarHidden = isLandscape;
    self.largeBackButton.hidden = !isLandscape;
    self.largeTitleLabel.hidden = !isLandscape;
    self.toolView.hidden = isLandscape;
    self.playerToolboxHeightConstraint.constant = isLandscape ? .0f : 60.0f;
    self.controlBarView.hidden = isLandscape;
    self.cloudAdImage.hidden = isLandscape;
    self.fecView.hidden = isLandscape;
    self.mAICloudStorageBottomConstraint.active = !isLandscape;///< 横屏时禁用container的bottom约束，释放布局空间
    // AI云存储横竖屏切换处理
    BOOL aiShowing = self.mAICloudStorageView.isShowing;
    if (isLandscape && aiShowing) {// 横屏且 AI云存储显示中 → 记录 + 隐藏
        self.mAICloudStorageShowingBeforeRotate = YES;
        [self.mAICloudStorageView hide];
        self.mAICloudStorageContainer.hidden = YES;
    } else if (!isLandscape && self.mAICloudStorageShowingBeforeRotate) {// 竖屏且之前记录了 → 恢复
        self.mAICloudStorageShowingBeforeRotate = NO;
        [self.mAICloudStorageView resume];
    }
    self.playbackList.hidden = isLandscape || self.mAICloudStorageView.isShowing;
    self.noVideoImageView.hidden = isLandscape ? YES : (self.records.count > 0);
}

#pragma mark - PlayerDelegate Methods | 播放器消息回调

/** 播放失败回调 */
- (void)player:(EZPlayer *)player didPlayFailed:(NSError *)error {
    [self invalidateTimer];
    [player stopPlayback];
    [self.loadingView stopSquareClockwiseAnimation];// 停止加载
    
    NSLog(@"player: %@ didPlayFailed: %@", player, error);
    if (error.code == EZ_SDK_NEED_VALIDATECODE) {// 需要验证码
        [self showSetPassword];
    } else if (error.code == EZ_SDK_VALIDATECODE_NOT_MATCH) {// 验证码错误
        [self showRetry];
    } else {
        [EZToast show:[NSString stringWithFormat:@"error code: %d, userInfo: %@", (int)error.code, error.userInfo]];
        self.messageLabel.text = [NSString stringWithFormat:@"%@(%d)",NSLocalizedString(@"device_play_fail", @"播放失败"), (int)error.code];
        self.messageLabel.hidden = NO;
        _playbackStatus = EZPlaybackStatusStop;
        [self.playButton setImage:[UIImage systemImageNamed:@"play"] forState:UIControlStateNormal];
    }
}

/** 播放成功回调 */
- (void)player:(EZPlayer *)player didReceivedMessage:(NSInteger)messageCode {
    NSLog(@"player: %@ didReceivedMessage: %d", player, (int)messageCode);
    if (messageCode == PLAYER_PREPARED) {// 播放准备
        // 如果开始回放时需立即开启倍数，须在此消息回调中调用setPlaybackRate，不要在PLAYER_PLAYBACK_START消息回调中设置。
        // 因为setPlaybackRate设置后也会有一个PLAYER_PLAYBACK_START消息通知，在PLAYER_PLAYBACK_START消息回调中设置会陷入循环
//        [self.player setPlaybackRate:EZOPENSDK_PLAY_RATE_4 mode:0];
        
        // 如果打开了纯音频播放配置，需要在此处打开声音
        if ([GlobalKit shareKit].enablePureAudio) {
            [self.player openSound];
        }
    } else if (messageCode == PLAYER_PLAYBACK_START) {// 录像回放开始|seek成功
        [self handlePlaybackStart];
    } else if (messageCode == PLAYER_PLAYBACK_STOP) {// 录像回放完成，回放完成后回调
        [self handlePlaybackFinish];
    } else if (messageCode == PLAYER_PLAYSPEED_LOWER) {// 回放倍速降低消息
        [EZToast show:@"回放倍速降速通知"];
        // TODO 4倍速以上则直接降速到4倍速，4倍速及其以下则直接降速到1倍速
    } else if (messageCode == PLAYER_NET_CHANGED) {
        [self startPlaybackByRecordCategory];
    }
}

/** 录像回放开始，更新UI */
- (void)handlePlaybackStart {
    _playbackStatus = EZPlaybackStatusPlaying;
    [self.playButton setImage:[UIImage systemImageNamed:@"pause.fill"] forState:UIControlStateNormal];
    [self.loadingView stopSquareClockwiseAnimation];
    self.sliderAreaView.hidden = NO;
    self.controlBarView.hidden = NO;
    [self setPlaybackControlButtonsEnabled:YES];// 暂停/播放、声音、下载、倍数按钮恢复为可点击状态
    self.messageLabel.hidden = YES;
    
    if (!_isOpenSound) {
        [self.player closeSound];
    }
    [self invalidateTimer];
    self.playbackTimer = [NSTimer scheduledTimerWithTimeInterval:1
                                                          target:self
                                                        selector:@selector(playBoxToolRefresh:)
                                                        userInfo:nil
                                                         repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:self.playbackTimer forMode:NSRunLoopCommonModes];
    [self performSelector:@selector(hiddenPlayerToolbox:) withObject:nil afterDelay:5.0f];
    [self showStreamFetchType];
    // AI云存储录像播放成功，通知AI云存储页面显示播放动画
    if (self.mIsAICloudStoragePlayback && self.mAICloudStorageView != nil) {
        [self.mAICloudStorageView updatePlaybackState:YES];
        // 如果有加密封面尚未加载（用户之前取消过验证码），此时验证码已获取，刷新封面
        if ([self.mAICloudStorageView hasEncryptedCoversNotLoaded]) {
            [self.mAICloudStorageView reloadEncryptedCovers:self.verifyCode];
        }
    }
    /// 鱼眼设备专用设置，如果没有鱼眼设备，不需要如下代码
    if ([FecViewLayoutHelper isFecDevice:self.deviceInfo]) {// 鱼眼设备预览矫正模式下会隐藏playerView，重新开启预览时需要重置。
        [self.fecViewLayoutHelper openFecCorrect:self.fecCorrectType fecPlaceType:self.fecPlaceType];
    }
    /// 鱼眼设备专用设置，如果没有鱼眼设备，不需要如上代码
}

/** 执行播放完毕的操作：如停止player、更新界面 */
- (void)handlePlaybackFinish {
    [self invalidateTimer];
    [self.player stopPlayback];// 停止回放，必须
    _playbackStatus = EZPlaybackStatusStop;
    self.sliderAreaView.hidden = YES;// 播放完毕隐藏进度条视图（当前片段播放时长、进度条、总时长）
    [self setPlaybackControlButtonsEnabled:NO];// 暂停/播放、声音、下载、倍数按钮设置为不可点击状态
    self.messageLabel.text = NSLocalizedString(@"tip_playback_finished", @"录像已全部播完");
    self.messageLabel.hidden = NO;
    
    // AI云存储录像播放完成，重置播放图标
    if (self.mIsAICloudStoragePlayback && self.mAICloudStorageView != nil) {
        [self.mAICloudStorageView updatePlaybackState:NO];
        self.mIsAICloudStoragePlayback = NO;
    }
}

#pragma mark - UICollectionViewDataSource Methods

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
    return [_records count];
}

- (NSInteger)collectionView:(UICollectionView *)collectionView layout:(DDCollectionViewFlowLayout *)layout numberOfColumnsInSection:(NSInteger)section {
    return 3;
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
    EZRecordCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"RecordCell" forIndexPath:indexPath];
    cell.deviceSerial = _cameraInfo.deviceSerial;
    
    if (_recordCategory == EZRecordCategoryDevice) {
        [cell setDeviceRecord:[_records dd_objectAtIndex:indexPath.row] selected:(indexPath.row == self.selectedIndexPath.row)];
    } else {
        [cell setCloudRecord:[_records dd_objectAtIndex:indexPath.row] selected:(indexPath.row == self.selectedIndexPath.row)];
    }
    return cell;
}

#pragma mark - UICollectionViewDelegate Methods

- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
    return CGSizeMake(106 * kScreenWidth / 320.0f, 62.0 * kScreenWidth / 320.0f);
}

- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
    if (!self.selectedCell) {
        self.selectedCell = YES;
        [self performSelector:@selector(repeatDelay) withObject:nil afterDelay:1.0f];
        
        // 判断取流链路已经存在，如果是则用 seekPlayback 跳转到选中片段
        BOOL isStreamConnected = _playbackStatus == EZPlaybackStatusPlaying || _playbackStatus == EZPlaybackStatusPause;
        if (isStreamConnected) {
            // 更新列表选中状态
            self.selectedIndexPath = indexPath;
            [collectionView reloadData];
            // seek 到选中片段的开始时间
            NSDate *seekTime = nil;
            if (_recordCategory == EZRecordCategoryDevice) {
                _deviceRecord = [_records dd_objectAtIndex:indexPath.row];
                seekTime = _deviceRecord.startTime;
            } else {
                _cloudRecord = [_records dd_objectAtIndex:indexPath.row];
                seekTime = _cloudRecord.startTime;
            }
            [self setPlaybackSliderUI];
            [self invalidateTimer];
            [_player seekPlayback:seekTime];
            [self.loadingView startSquareClcokwiseAnimation];
            return;
        }
        
        [self invalidateTimer];
        [_player stopPlayback];
        _playbackStatus = EZPlaybackStatusStop;
        
        if (_recordCategory == EZRecordCategoryDevice) {
            _deviceRecord = [_records dd_objectAtIndex:indexPath.row];
            // 将结束时间扩展为列表中最近录像的结束时间，实现连续回放
            NSDate *latestStopTime = [EZBusinessTool getLatestStopTimeFromDeviceRecords:_records];
            if (latestStopTime) {
                _deviceRecord.stopTime = latestStopTime;
            }
        } else {
            _cloudRecord = [_records dd_objectAtIndex:indexPath.row];
            // 将结束时间扩展为列表中最近录像的结束时间，实现连续回放
            NSDate *latestStopTime = [EZBusinessTool getLatestStopTimeFromCloudRecords:_records];
            if (latestStopTime) {
                _cloudRecord.stopTime = latestStopTime;
            }
        }
        
        self.selectedIndexPath = indexPath;
        [collectionView reloadData];
        
        [self setPlaybackSliderUI];
        self.messageLabel.hidden = YES;
        [self.loadingView startSquareClcokwiseAnimation];
        [self.rateBtn setTitle:@"1倍" forState:UIControlStateNormal];
        
        [self startPlaybackByRecordCategory];
    }
}

#pragma mark - RecordCoverFetcherDelegate | SD卡录像封面获取回调

/** SD卡录像封面提取封面成功回调 */
- (void)onGetCoverSuccess:(int)seq data:(NSData *_Nonnull)data {
    // 注意：图片是设备一张一张传回来的，接收到一张就需要局部刷新UI。
    // 本demo将data转换为UIImage直接加载。开发者也可自行将data转为文件，进行缓存管理。
    
    // 以下情况做拦截，否则会将SD卡录像封面显示在云存储录像上或者数组越界崩溃
    if (_recordCategory != EZRecordCategoryDevice || kArrayIsEmpty(self.records) || seq > self.records.count-1) {
        return;
    }
    EZDeviceRecordFile *recordFile = self.records[seq];
    if (![recordFile isKindOfClass:[EZDeviceRecordFile class]]) {
        return;
    }
    recordFile.imageData = data;
    
    [self.playbackList reloadItemsAtIndexPaths:@[[NSIndexPath indexPathForRow:seq inSection:0]]];
}

/** SD卡录像封面提取封面失败回调 */
- (void)onGetCoverFailed:(int)errorCode {
    NSLog(@"onGetCoverFailed");
}

#pragma mark - EZAICloudStorageViewDelegate

- (void)onAICloudRecordFileClick:(EZCloudRecordFile *)recordFile {
    NSLog(@"AI云录像片段点击，开始播放: fileId=%@", recordFile.fileId);
    self.mIsAICloudStoragePlayback = YES;
    // 停止当前播放
    if (_player) {
        [self invalidateTimer];
        [_player stopPlayback];
    }
    // 设置云录像信息并启动回放
    _cloudRecord = recordFile;
    _recordCategory = EZRecordCategoryCloud;
    
    [self startPlaybackByRecordCategory];
    
    [self setPlaybackSliderUI];
    self.messageLabel.hidden = YES;
    [self.loadingView startSquareClcokwiseAnimation];
    [self.rateBtn setTitle:@"1倍" forState:UIControlStateNormal];
}

- (void)onAICloudStorageViewNeedVerifyCode {
    NSLog(@"AI云存储封面加密，需要验证码");
    // 如已有验证码，直接使用
    if (self.verifyCode.length > 0) {
        [self.mAICloudStorageView reloadEncryptedCovers:self.verifyCode];
        return;
    }
    // 否则弹出验证码输入框
    [UIAlertController showAlertWithDeviceSerial:self.cameraInfo.deviceSerial confirmBlock:^(NSString *code) {
        self.verifyCode = code;
        if (self.player) {
            [self.player setPlayVerifyCode:code];
        }
        [self.mAICloudStorageView reloadEncryptedCovers:code];
    }];
}

#pragma mark - MJRefresh Methods

- (void)addHeaderRefresh {
    [self.operation cancel];
    [self.playbackList.header endRefreshing];
    EZWeak(self);
    self.playbackList.header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{
        EZStrong(self);
        strongself.noVideoImageView.hidden = YES;
        strongself.noVideoLabel.hidden = YES;
        strongself.selectedIndexPath = nil;
        if (strongself.recordCategory == EZRecordCategoryDevice) {
            // 注意：国内服务器支持videoRecordType字段，海外服务器不支持
            strongself.operation = [EZOPENSDK searchRecordFileFromDevice:strongself.cameraInfo.deviceSerial
                                                                cameraNo:strongself.cameraInfo.cameraNo
                                                               beginTime:strongself.beginTime
                                                                 endTime:strongself.endTime
#ifdef EZVIZ_OPEN_DEMO
                                                         videoRecordType:strongself->recordType
#endif
                                                              completion:^(NSArray *deviceRecords, NSError *error) {
                [strongself refreshCollectionViewWithRecords:deviceRecords error:error];
            }];
        } else if (strongself.recordCategory == EZRecordCategoryCloud) {
            strongself.operation = [EZOPENSDK searchRecordFileFromCloud:strongself.cameraInfo.deviceSerial
                                                               cameraNo:strongself.cameraInfo.cameraNo
                                                              beginTime:strongself.beginTime
                                                                endTime:strongself.endTime
                                                             completion:^(NSArray *cloudRecords, NSError *error) {
                [strongself refreshCollectionViewWithRecords:cloudRecords error:error];
            }];
        } else if (strongself.recordCategory == EZRecordCategorySDKCloud) {
            strongself.operation = [EZOPENSDK searchRecordFileFromSDKCloudRecord:strongself.cameraInfo.deviceSerial
                                                                        cameraNo:strongself.cameraInfo.cameraNo
                                                                       beginTime:strongself.beginTime
                                                                         endTime:strongself.endTime
                                                                         spaceId:[kUserDefaults objectForKey:EZCloudRecordSpaceId]
                                                                      completion:^(NSArray *records, NSError *error) {
                [strongself refreshCollectionViewWithRecords:records error:error];
            }];
        }
    }];
    self.playbackList.header.automaticallyChangeAlpha = YES;
    [self.playbackList.header beginRefreshing];
}

/** 获取到录像列表数据后再刷新 */
- (void)refreshCollectionViewWithRecords:(NSArray *)records error:(NSError *)error {
    [self.playbackList.header endRefreshing];
    if (error) {
        [UIView dd_showMessage:[NSString stringWithFormat:@"error code is %d",(int) error.code] onParentView:self.view];
        [self.records removeAllObjects];
        [self.playbackList reloadData];
        return;
    }
    [self.records removeAllObjects];
    if (records.count == 0) {
        self.noVideoLabel.hidden = NO;
        self.noVideoImageView.hidden = NO;
        [self.playbackList reloadData];
        return;
    }
    if (records.count > 0) {
        [self.records addObjectsFromArray:records];
        self.records = [EZCommonTool reverseArray:self.records];
        [self.playbackList reloadData];
        [self doPlayback];
#ifdef EZVIZ_OPEN_DEMO
        // 如果是SD卡录像，请求录像封面
        if (_recordCategory == EZRecordCategoryDevice) {
            [self requestDeviceRecordCover];
        }
#endif
    }
}

/** 请求录像封面 */
- (void)requestDeviceRecordCover {
    for (int i = 0; i < self.records.count; i ++) {
        EZDeviceRecordFile *recordFile = self.records[i];
        recordFile.seq = i;// 设置索引，封面回调的时候知道对应哪一个录像
    }
    // 去获取SD卡视频封面
    BOOL isSupportSdCover = [EZBusinessTool isSupportSdCover:self.deviceInfo cameraInfo:self.cameraInfo];
    if (isSupportSdCover) {
        [[EZRecordCoverFetcherManager sharedInstance] requestRecordCover:self.records];
    }
}

#pragma mark - Download Methods | 录像下载方法

- (void)startDeviceRecordDownload:(NSString *)path deviceFile:(EZDeviceRecordFile *)deviceFile {
    TICK
    // 创建下载任务
    [[EZDeviceRecordDownloadTask alloc] initTaskWithID:_selectedIndexPath.row
                                  DeviceRecordFileInfo:deviceFile
                                          deviceSerial:_cameraInfo.deviceSerial
                                              cameraNo:self.cameraInfo.cameraNo
                                            verifyCode:self.verifyCode
                                              savePath:path
                                            completion:^(EZDeviceRecordDownloadTask * _Nonnull task) {
        // 设置回调函数
        __weak typeof(task) weakTask = task;
        if (self.isMultiChannelDevice) {
            [task setMultiChannelDevice:YES];
        }
        [task setDownloadCallBackWithFinshed:^(EZRecordDownloaderStatus statusCode) {
            __strong typeof(weakTask) strongTask = weakTask;
            NSLog(@"statuCode:%ld", (long)statusCode);
            
            switch (statusCode) {
                case EZRecordDownloaderStatusFinish:
                {
                    TOCK
                    [EZToast show:[NSString stringWithFormat:@"SDD Task:%lu-下载成功，下载时长：%f 秒", (long)strongTask.taskID, -[startTime timeIntervalSinceNow]]];
                    NSString *mp4Path = [NSString stringWithFormat:@"%@/%@.mp4", PATH_DeviceRecord, [deviceFile.startTime formattedDateWithFormat:@"yyyyMMddHHmmss"]];
                    [self videoFormatConvertAndSave2Album:path mp4Path:mp4Path];
                }
                    break;
                case EZRecordDownloaderStatusMoreToken:
                    NSLog(@"EZRecordDownloaderStatusMoreToken.");
                    break;
                default:
                    
                    break;
            }
            [[EZRecordDownloader shareInstane] stopDownloadTask:strongTask];
        } failed:^(NSError * _Nonnull error) {
            NSLog(@"EZDeviceRecordDownloader error:%@", error);
            __strong typeof(weakTask) strongTask = weakTask;
            
            if (error.code == 395416 || error.code == 380045) {
                [EZToast show:[NSString stringWithFormat:@"SDD Task:%lu-下载限制，达到最大连接数", (long)strongTask.taskID]];
            } else {
                [EZToast show:[NSString stringWithFormat:@"SDD Task:%lu-下载失败", (long)strongTask.taskID]];
            }
            [[EZRecordDownloader shareInstane] stopDownloadTask:strongTask];
        }];
        // SD卡本地录像不支持setDownloadCallBackWithDownloadSize下载进度回调
        
        if (task) {
            [EZToast show:[NSString stringWithFormat:@"SDD Task:%lu-开始下载", (long)task.taskID]];
            // 加载SD卡下载特殊配置，一般开发者可不用配置，按需设置
            [EZStreamConfigLoader loadDownloadDeviceVideoSpecialConfigToStreamDownload:task];
            // 加入下载队列下载
            int ret = [[EZRecordDownloader shareInstane] addDownloadTask:task];
            if (ret == -1) {
                [EZToast show:[NSString stringWithFormat:@"SDD Task:%lu-任务为空", (long)task.taskID]];
            } else if (ret == -2) {
                [EZToast show:[NSString stringWithFormat:@"SDD Task:%lu-任务已在下载中", (long)task.taskID]];
            }
        }
    }];
}

- (void)startCloudRecordDownload:(NSString *)path cloudFile:(EZCloudRecordFile *)cloudFile {
    TICK
    EZCloudRecordDownloadTask *task = [[EZCloudRecordDownloadTask alloc] initTaskWithID:_selectedIndexPath.row cloudRecordFile:cloudFile verifyCode:self.verifyCode savePath:path];
    
    if (self.isMultiChannelDevice) {
        [task setMultiChannelDevice:YES];
    }
    // 设置回调函数
    __weak typeof(task) weakTask = task;
    [task setDownloadCallBackWithFinshed:^(EZRecordDownloaderStatus statusCode) {
        __strong typeof(weakTask) strongTask = weakTask;
        NSLog(@"statuCode:%ld", (long)statusCode);
        
        switch (statusCode) {
            case EZRecordDownloaderStatusFinish:
            {
                TOCK
                [EZToast show:[NSString stringWithFormat:@"CD Task:%lu-下载成功，下载时长：%f 秒", (long)strongTask.taskID, -[startTime timeIntervalSinceNow]]];
                NSString *mp4Path = [NSString stringWithFormat:@"%@/%@.mp4", PATH_CloudRecord, [cloudFile.startTime formattedDateWithFormat:@"yyyyMMddHHmmss"]];
                [self videoFormatConvertAndSave2Album:path mp4Path:mp4Path];
            }
                break;
            case EZRecordDownloaderStatusMoreToken:
                NSLog(@"EZRecordDownloaderStatusMoreToken.");
                break;
            default:
                break;
        }
        [[EZRecordDownloader shareInstane] stopDownloadTask:strongTask];
        
    } failed:^(NSError * _Nonnull error) {
        NSLog(@"EZDeviceRecordDownloader error:%@", error);
        __strong typeof(weakTask) strongTask = weakTask;
        [EZToast show:[NSString stringWithFormat:@"CD Task:%lu-下载失败", (long)strongTask.taskID]];
        [[EZRecordDownloader shareInstane] stopDownloadTask:strongTask];
    }];
    // 设置下载进度回调
    [task setDownloadCallBackWithDownloadSize:^(NSUInteger downloadSize) {
        NSLog(@"%@ download percent--->%lu",cloudFile.fileId, (long)(downloadSize*100/cloudFile.fileSize));
    }];
    
    if (task) {
        [EZToast show:[NSString stringWithFormat:@"CD Task:%lu-开始下载", (long)task.taskID]];
        // 加入下载队列下载
        int ret = [[EZRecordDownloader shareInstane] addDownloadTask:task];
        if (ret == -1) {
            [EZToast show:[NSString stringWithFormat:@"CD Task:%lu-任务为空", (long)task.taskID]];
        } else if (ret == -2) {
            [EZToast show:[NSString stringWithFormat:@"CD Task:%lu-任务已在下载中", (long)task.taskID]];
        }
    }
}

/** 如果需要把回放视频保存到相册，一定要先把.ps文件转成.mp4文件
 注意：下载录像过程中因为网络原因导致.ps文件未下载完，也可以调用此方法将已下载的录像片段解码成.mp4文件。
 */
- (void)videoFormatConvertAndSave2Album:(NSString *)psPath mp4Path:(NSString *)mp4Path {
    [FCFileManager createFileAtPath:mp4Path overwrite:YES];
    TICK
    /**
     * 单目设备：ps文件中只有一路码流，转封装一次即可【streamId传0；streamStateVariable传NO】
     * 双目设备：ps文件中有两路码流，需装封装两次；【streamId传0和1，对应画面轨道；streamStateVariable传YES】
     */
    int count = self.isMultiChannelDevice ? 2 : 1;
    for (int i = 0; i < count; i ++) {
        NSString *outFilePath = mp4Path;
        // 创建第二个轨道转封装路径
        if (i == 1) {
            NSRange range = [mp4Path rangeOfString:@"." options:NSBackwardsSearch];
            if (range.location == NSNotFound) {
                NSLog(@"videoFormatConvertAndSave2Album: not found symbol . in mp4Path");
                return;
            }
            outFilePath = [NSString stringWithFormat:@"%@_1%@", [mp4Path substringToIndex:range.location], [mp4Path substringFromIndex:range.location]];
            [FCFileManager createFileAtPath:outFilePath overwrite:YES];
        }
        [EZVideoTransformer videoTransFormerPSPath:psPath toPath:outFilePath type:EZVideoTransformerTypeMP4 withKey:_verifyCode streamId:i streamStateVariable:self.isMultiChannelDevice succBlock:^{
            NSLog(@"转换成功");
            // 将.mp4文件保存到相册
            PHPhotoLibrary *photoLibrary = [PHPhotoLibrary sharedPhotoLibrary];
            [photoLibrary performChanges:^{
                [PHAssetChangeRequest creationRequestForAssetFromVideoAtFileURL:[NSURL fileURLWithPath:outFilePath]];
            } completionHandler:^(BOOL success, NSError * _Nullable error) {
                if (success) {
                    if (self.isMultiChannelDevice && i == 0) {
                        // 双通道设备，第一个轨道转封装成功后暂不处理，还需要等待第二个轨道转封装完成
                    } else {
                        TOCK
                        [EZToast show:[NSString stringWithFormat:@"已将视频保存至相册，转码时长：%f 秒", -[startTime timeIntervalSinceNow]]];
                        // 下载完成后删除.ps和.mp4文件
                        [FCFileManager removeItemAtPath:psPath];
                        [FCFileManager removeItemAtPath:mp4Path];
                    }
                } else {
                    [EZToast show:@"未能保存视频到相册"];
                }
            }];
        } processBlock:^(int rate) {
            // 转换进度
        } failBlock:^(int errCode) {
            NSLog(@"转换失败");
        }];
    }
}

#pragma mark - ValidateCode Methods | 验证码检验

- (void)showSetPassword {
    [UIAlertController showAlertWithDeviceSerial:_cameraInfo.deviceSerial confirmBlock:^(NSString * _Nonnull code) {
        self.verifyCode = code;
        [self.player setPlayVerifyCode:code];
        [self doPlayback];
    }];
}

- (void)showRetry {
    [UIAlertController showDeviceSerialRetryAlert:^{
        [self showSetPassword];
    }];
}

#pragma mark - Actions | 点击事件

/** 全屏回放Action */
- (IBAction)large:(id)sender {
    /// 鱼眼设备专用设置，如果没有鱼眼设备，不需要如下代码
    if ([FecViewLayoutHelper isFecDevice:self.deviceInfo]) {
        [self fecViewShow];
        return;
    }
    /// 鱼眼设备专用设置，如果没有鱼眼设备，不需要如上代码
    [EZScreenOrientationTool landscape:self];
}

/** 全屏回放退出Action */
- (IBAction)largeBack:(id)sender {
    [EZScreenOrientationTool portrait:self];
}

/** 点击播放器显示播放器工具栏 */
- (IBAction)showToolBar:(id)sender {
    if (!_isShowToolbox) {
        _isShowToolbox = YES;
        [UIView animateWithDuration:0.3 animations:^{
            self.playerToolbox.alpha = 1.0f;
        }];
        [self performSelector:@selector(hiddenPlayerToolbox:) withObject:nil afterDelay:5.0f];
    } else {
        [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(hiddenPlayerToolbox:) object:nil];
        [self hiddenPlayerToolbox:nil];
    }
}

/** 点击播放器隐藏播放器工具栏 */
- (IBAction)hiddenPlayerToolbox:(id)sender {
    //    [UIView animateWithDuration:0.3
    //                     animations:^{
    //                         self.playerToolbox.alpha = 0.0f;
    //                     }
    //                     completion:^(BOOL finished) {
    //                         _isShowToolbox = NO;
    //                     }];
}

/** 播放|暂停Action */
- (IBAction)playButtonClicked:(id)sender {
    if (_playbackStatus == EZPlaybackStatusPlaying) {
        [_player pausePlayback];
        _playbackStatus = EZPlaybackStatusPause;
        [self.playButton setImage:[UIImage systemImageNamed:@"play"] forState:UIControlStateNormal];
        if (_playbackTimer && _recordCategory != EZRecordCategoryDevice) {
            [_playbackTimer invalidate];
            _playbackTimer = nil;
        }
    } else {
        if (!_cloudRecord && !_deviceRecord) {
            return;
        }
        [_player resumePlayback];// 执行此方法后会回调player:didReceivedMessage:
        _playbackStatus = EZPlaybackStatusPlaying;
        [self.playButton setImage:[UIImage systemImageNamed:@"pause.fill"] forState:UIControlStateNormal];
        self.messageLabel.hidden = YES;
    }
}

/** 开启/关闭声音Action */
- (IBAction)voiceButtonClicked:(id)sender {
    if (_isOpenSound) {
        [_player closeSound];
        [self.voiceButton setImage:[UIImage systemImageNamed:@"speaker.slash"] forState:UIControlStateNormal];
    } else {
        [_player openSound];
        [self.voiceButton setImage:[UIImage systemImageNamed:@"speaker.wave.2"] forState:UIControlStateNormal];
    }
    _isOpenSound = !_isOpenSound;
}

/** 录像下载Action */
- (IBAction)clickDownloadBtn:(id)sender {
    NSString *dateFormat = @"yyyyMMddHHmmss";
    if (_recordCategory == EZRecordCategoryDevice) {
        EZDeviceRecordFile *deviceFile = (EZDeviceRecordFile *)[_records dd_objectAtIndex:self.selectedIndexPath.row];
        NSString *path = [NSString stringWithFormat:@"%@/%@.ps", PATH_DeviceRecord, [deviceFile.startTime formattedDateWithFormat:dateFormat]];
        NSLog(@"path: %@ ", path);
        [self startDeviceRecordDownload:path deviceFile:deviceFile];
    } else {
        EZCloudRecordFile *cloudFile = (EZCloudRecordFile *)[_records dd_objectAtIndex:self.selectedIndexPath.row];
        NSString *path = [NSString stringWithFormat:@"%@/%@.ps", PATH_CloudRecord, [cloudFile.startTime formattedDateWithFormat:dateFormat]];
        NSLog(@"path: %@ ", path);
        [self startCloudRecordDownload:path cloudFile:cloudFile];
    }
}

/** 倍速播放选择弹框Action */
- (IBAction)clickRateBtn:(UIButton *)sender {
    if (_recordCategory == EZRecordCategoryDevice) {
        [self showSDCardRateActionSheet];
    } else {
        [self showCloudRateActionSheet];
    }
}

/** AI云存储按钮点击 */
- (IBAction)aiCloudStorageBtnClicked:(id)sender {
    if (self.mAICloudStorageView && self.mAICloudStorageView.isShowing) {
        // 已展示则隐藏
        [self.mAICloudStorageView hide];
        self.playbackList.hidden = NO;
    } else if (self.mAICloudStorageView) {
        // 已创建过，恢复展示（错误状态会重新请求接口）
        [self.mAICloudStorageView resume];
        self.playbackList.hidden = YES;
    } else {
        // 首次创建，调用EZOpenSDK的API展示AI云存储
        self.mAICloudStorageView = [EZOpenSDK showAICloudStorage:self.mAICloudStorageContainer
                                                    deviceSerial:self.cameraInfo.deviceSerial
                                                        cameraNo:self.cameraInfo.cameraNo
                                                      verifyCode:self.verifyCode];
        self.mAICloudStorageView.delegate = self;
        [self.view bringSubviewToFront:self.mAICloudStorageContainer];
        self.playbackList.hidden = YES;// iOS 手机底部有安全区域，录像列表在安全区域内也是显示的，需要隐藏
    }
}

/** 播放进程条拖动 */
- (IBAction)duringValueChanged:(id)sender {
    NSTimeInterval playSeconds = _duringSeconds * self.duringSlider.value;
    self.playTimeLabel.text = [EZCommonTool convToUIDuration:playSeconds];
}

/** 播放进程条拖动结束后 */
- (IBAction)duringValueChange:(id)sender {
    float progress = self.duringSlider.value;
    if (progress == 1) {
        [self handlePlaybackFinish];
        return;
    }
    NSDate *offsetTime = nil;
    if (_recordCategory == EZRecordCategoryDevice) {
        offsetTime = [_deviceRecord.startTime dateByAddingTimeInterval:_duringSeconds * self.duringSlider.value];
    } else {
        offsetTime = [_cloudRecord.startTime dateByAddingTimeInterval:_duringSeconds * self.duringSlider.value];
    }
    [self invalidateTimer];
    [_player seekPlayback:offsetTime];
    [self.loadingView startSquareClcokwiseAnimation];
}

/** 云存储Tab Action */
- (IBAction)cloudButtonClicked:(id)sender {
    _recordCategory = EZRecordCategoryCloud;
    [self recordCategoryClickedReset:sender selectedImageViewConstraint:0];
    
    [self.recordTypeButton setBackgroundImage:[UIImage systemImageNamed:@"slider.horizontal.3"] forState:UIControlStateNormal];
    self.recordTypeButton.hidden = _isCloudRightNavHidden;
    self.downloadButton.hidden = NO;
}

/** SDK云录制Tab Action */
- (IBAction)sdkCloudRecordClicked:(id)sender {
    _recordCategory = EZRecordCategorySDKCloud;
    [self recordCategoryClickedReset:sender selectedImageViewConstraint:self.view.bounds.size.width * 1 / 3];
    
    [self.recordTypeButton setBackgroundImage:GetImage(@"") forState:UIControlStateNormal];
    self.recordTypeButton.hidden = YES;
    self.downloadButton.hidden = YES;// 云录制录像不支持下载
}

/** SD卡录像Tab Action */
- (IBAction)deviceButtonClicked:(id)sender {
    _recordCategory = EZRecordCategoryDevice;
    [self recordCategoryClickedReset:sender selectedImageViewConstraint:self.view.bounds.size.width * 2 / 3];
    
    [self.recordTypeButton setBackgroundImage:[UIImage systemImageNamed:@"square.grid.2x2"] forState:UIControlStateNormal];
    self.recordTypeButton.hidden = NO;
    BOOL isSupportSDRecordDownload = [EZBusinessTool isSupportSDRecordDownload:self.deviceInfo cameraInfo:self.cameraInfo];
    self.downloadButton.hidden = !isSupportSDRecordDownload;
}

/** 录像类别Tab点击后重置 */
- (void)recordCategoryClickedReset:(UIButton *)selectButton selectedImageViewConstraint:(CGFloat)constant {
    if (self.player) {
        [self invalidateTimer];
        [self.player stopPlayback];
    }
    self.selectedIndexPath = nil;
    [UIView animateWithDuration:0.3 animations:^{
        self.selectedImageViewConstraint.constant = constant;
        [self.toolView setNeedsUpdateConstraints];
        [self.toolView layoutIfNeeded];
    } completion:^(BOOL finished) {
        self.cloudButton.selected = NO;
        self.sdkCloudRecordButton.selected = NO;
        self.deviceButton.selected = NO;
        
        selectButton.selected = YES;
    }];
    [self.records removeAllObjects];
    [self.playbackList reloadData];
    [self addHeaderRefresh];
}

/** 顶部日期选择—取消Action */
- (IBAction)cancel:(id)sender {
    [self.dateTextField resignFirstResponder];
    self.dateButton.selected = NO;
}

/** 顶部日期选择—确定Action */
- (IBAction)confirm:(id)sender {
    [self.dateTextField resignFirstResponder];
    NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
    dateFormatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"zh_CN"];
    dateFormatter.dateFormat = @"MM-dd ▽";
    [self.dateButton setTitle:[dateFormatter stringFromDate:self.datePicker.date] forState:UIControlStateNormal];
    dateFormatter.dateFormat = @"MM-dd △";
    [self.dateButton setTitle:[dateFormatter stringFromDate:self.datePicker.date] forState:UIControlStateSelected];
    self.dateButton.selected = NO;
    
    [self.playbackList.header beginRefreshing];
}

/** 顶部日期选择Action */
- (IBAction)dateButtonClicked:(id)sender {
    [self.dateTextField becomeFirstResponder];
    self.datePicker.maximumDate = [NSDate date];
    self.dateButton.selected = YES;
}

/** 录像播放进度UI初始化（已播放时长、总时长、UISlider进度） */
- (void)setPlaybackSliderUI {
    if (_recordCategory == EZRecordCategoryDevice) {
        _duringSeconds = [_deviceRecord.stopTime timeIntervalSinceDate:_deviceRecord.startTime];
    } else {
        _duringSeconds = [_cloudRecord.stopTime timeIntervalSinceDate:_cloudRecord.startTime];
    }
    
    self.duringTimeLabel.text = [EZCommonTool convToUIDuration:_duringSeconds];
    self.playTimeLabel.text = _duringSeconds >= 3600 ? @"00:00:00" : @"00:00";
    self.duringSlider.value = 0;
}

/** 刷新播放进度 */
- (void)playBoxToolRefresh:(NSTimer *)timer {
    NSDate *currentTime = [_player getOSDTime];
    if (!currentTime) {
        return;
    }
    NSLog(@"getOSDTime === %@", currentTime);
    
    // 根据 OSD 时间刷新录像选中状态
    [self updateSelectedRecordByOSD:currentTime];
    
    if (_recordCategory == EZRecordCategoryDevice) {
        _playSeconds = [currentTime timeIntervalSinceDate:_deviceRecord.startTime];
    } else {
        _playSeconds = [currentTime timeIntervalSinceDate:_cloudRecord.startTime];
    }
    
    NSLog(@"_playSeconds === %f", _playSeconds);
    if (_playSeconds > 0) {
        self.playTimeLabel.text = [EZCommonTool convToUIDuration:_playSeconds];
        self.duringSlider.value = _playSeconds/_duringSeconds;
    }
}

/** 根据 OSD 时间戳更新录像列表的选中状态，当连续回放跨越片段时自动切换 */
- (void)updateSelectedRecordByOSD:(NSDate *)osdTime {
    if (!osdTime || !_records || _records.count == 0) return;
    
    // 检查 OSD 时间是否仍在当前片段范围内
    NSDate *currentStart = nil;
    NSDate *currentStop = nil;
    if (_recordCategory == EZRecordCategoryDevice) {
        currentStart = _deviceRecord.startTime;
        currentStop = _deviceRecord.stopTime;
    } else {
        currentStart = _cloudRecord.startTime;
        currentStop = _cloudRecord.stopTime;
    }
    if ([osdTime compare:currentStart] != NSOrderedAscending &&
        [osdTime compare:currentStop] != NSOrderedDescending) {
        return; // 仍在当前片段范围内，不需要切换
    }
    
    // 查找 OSD 时间所属的录像片段
    NSUInteger index = NSNotFound;
    if (_recordCategory == EZRecordCategoryDevice) {
        index = [EZBusinessTool findDeviceRecordIndexByTime:osdTime records:_records];
        if (index != NSNotFound) {
            _deviceRecord = _records[index];
        }
    } else {
        index = [EZBusinessTool findCloudRecordIndexByTime:osdTime records:_records];
        if (index != NSNotFound) {
            _cloudRecord = _records[index];
        }
    }
    
    if (index != NSNotFound) {
        self.selectedIndexPath = [NSIndexPath indexPathForRow:index inSection:0];
        [self.playbackList reloadData];
        [self setPlaybackSliderUI];
    }
}

/** 云存储图片点击 */
- (void)clickCloudAd {
    _isPlayingWhenJump = (_playbackStatus == EZPlaybackStatusPlaying);
    [EZOPENSDK openCloudPage:self.deviceInfo.deviceSerial channelNo:_cameraInfo.cameraNo];
}

/** 鱼眼设备模式矫正-页面显示Action */
- (void)fecViewShow {
    self.fecView.hidden = NO;
    BOOL supportWallType = [FecViewLayoutHelper getSupportInt:EZFecPlaceTypeWall deviceInfo:self.deviceInfo] > 0;
    BOOL supportFloorType = [FecViewLayoutHelper getSupportInt:EZFecPlaceTypeFloor deviceInfo:self.deviceInfo] > 0;
    BOOL supportCeilingType = [FecViewLayoutHelper getSupportInt:EZFecPlaceTypeCeiling deviceInfo:self.deviceInfo] > 0;
    self.placeWallBtn.hidden = !supportWallType;
    self.placeFloorBtn.hidden = !supportFloorType;
    self.placeCeilingBtn.hidden = !supportCeilingType;
    [self setFecCorrectTypeBtnsEnable:self.fecPlaceType];
    [self.view bringSubviewToFront:self.fecView];
    [UIView animateWithDuration:0.3
                     animations:^{
                         self.fecViewContraint.constant = 0;
                         self.fecView.alpha = 1.0;
                         [self.view setNeedsUpdateConstraints];
                         [self.view layoutIfNeeded];
                     }
                     completion:^(BOOL finished) {
                     }];
}

/** 鱼眼矫正模式-页面关闭Action */
- (IBAction)closeFecView:(id)sender {
    CGFloat height = self.toolView.frame.size.height+self.playbackList.frame.size.height+self.cloudAdImage.frame.size.height;
    [UIView animateWithDuration:0.3
                     animations:^{
                         self.fecView.alpha = 0.0;
                         self.fecViewContraint.constant = height;
                         [self.view setNeedsUpdateConstraints];
                         [self.view layoutIfNeeded];
                     }
                     completion:^(BOOL finished) {
                         self.fecView.alpha = 0;
                         self.fecView.hidden = YES;
                     }];
}

/** 鱼眼设备安装模式Action */
- (IBAction)fecPlaceTypeAction:(id)sender {
    UIButton *btn = (UIButton *)sender;
    self.fecPlaceType = (EZFecPlaceType)btn.tag;
    [self setFecCorrectTypeBtnsEnable:self.fecPlaceType];
}

/** 根据安装模式和能力集设置哪些矫正模式可用 */
- (void)setFecCorrectTypeBtnsEnable:(EZFecPlaceType)fecPlaceType {
    // 获取安装模式对应的能力集值
    int supportValue = [FecViewLayoutHelper getSupportInt:fecPlaceType deviceInfo:self.deviceInfo];
    [FecViewLayoutHelper setFecCorrectButtonsState:self.fecCorrectTypeButtons supportValue:supportValue];
}

/** 鱼眼设备矫正模式Action */
- (IBAction)fecCorrectTypeAction:(id)sender {
    UIButton *btn = (UIButton *)sender;
    // storyboard中给每个矫正模式按钮配置好了tag值，tag=该矫正模式枚举值
    self.fecCorrectType = (EZFecCorrectType)btn.tag;
    [self.fecViewLayoutHelper openFecCorrect:self.fecCorrectType fecPlaceType:self.fecPlaceType];
}

/** 云存储扩展接口 || SD卡视频类型点击事件 */
- (void)rightNavClicked {
    // 当前选中的标签为"SD卡录像"
    if (_recordCategory == EZRecordCategoryDevice) {
        FTPopOverMenuConfiguration *configuration = [[FTPopOverMenuConfiguration alloc] init];
        configuration.backgroundColor = [UIColor whiteColor];
        configuration.coverBackgroundColor = RGBAColor(0, 0, 0, 0.4);
        configuration.textColor = [UIColor blackColor];
        configuration.borderColor = [UIColor groupTableViewBackgroundColor];
        configuration.separatorColor = [UIColor groupTableViewBackgroundColor];
        configuration.menuIconMargin = 10.f;
        configuration.menuTextMargin = 10.f;
        configuration.selectedTextColor = [UIColor blackColor];
        configuration.selectedCellBackgroundColor = RGBAColor(0, 0, 0, 0.2);
        
        [FTPopOverMenu showForSender:self.recordTypeButton
                       withMenuArray:self.recordTypeMenuArray
                          imageArray:@[@"", @"", @""]
                       configuration:configuration
                           doneBlock:^(NSInteger selectedIndex) {
            recordType = (EZVideoRecordType)selectedIndex;
            [self.playbackList.header beginRefreshing];
        }
                        dismissBlock:^{
            NSLog(@"user canceled. do nothing.");
        }];
    } else {
        // 海外支持，国内不支持
#ifdef EZVIZ_GLOBAL_DEMO
        EZAbroadCloudServiceExVC *cloudServiceExVC = [[EZAbroadCloudServiceExVC alloc] init];
        cloudServiceExVC.cameraInfo = self.cameraInfo;
        [self.navigationController pushViewController:cloudServiceExVC animated:YES];
#endif
        
    }
}

#pragma mark - Electronic Zoom - Play Screen Zoom Event | 电子放大-播放画面缩放事件

/**
 * 单指拖移手势
 */
- (void)panGestureRecognizer:(UIPanGestureRecognizer *)gestureRecognizer {
    if (gestureRecognizer.state == UIGestureRecognizerStateBegan) {
        // 记录初始触摸点
        self.movePoint = [gestureRecognizer locationInView:self.playerView];
    } else if (gestureRecognizer.state == UIGestureRecognizerStateChanged || gestureRecognizer.state == UIGestureRecognizerStateEnded) {
        // 计算当前触摸点和初始触摸点的差值
        CGPoint currentTouchPoint = [gestureRecognizer locationInView:self.playerView];
        CGFloat deltaX = currentTouchPoint.x - self.movePoint.x;
        CGFloat deltaY = currentTouchPoint.y - self.movePoint.y;
        
        CGFloat videoDeltaX = -deltaX * self.zoomRect->size.width / self.playerView.bounds.size.width;
        CGFloat videoDeltaY = -deltaY * self.zoomRect->size.height / self.playerView.bounds.size.height;

        self.zoomRect->origin.x += videoDeltaX;
        self.zoomRect->origin.y += videoDeltaY;
        // 修正显示范围
        [EZBusinessTool judgeRect:self.zoomRect];
        // 电子放大，双目设备第二个镜头streamId传1
        [self.player setDisplayRegionEx:self.zoomRect streamId:0];
        // 如果手势结束，重置初始触摸点
        self.movePoint = currentTouchPoint;
    }
}

/**
 * 双指捏合手势
 */
- (void)pinchGestureRecognizer:(UIPinchGestureRecognizer *)gestureRecognizer {
    if (gestureRecognizer.state == UIGestureRecognizerStateChanged) {
        if (gestureRecognizer.numberOfTouches != 2) {
            return;
        }
        // 获取双指坐标
        CGPoint pointA = [gestureRecognizer locationOfTouch:0 inView:self.playerView];
        CGPoint pointB = [gestureRecognizer locationOfTouch:1 inView:self.playerView];
        // 获取双指中心点坐标
        CGFloat midX = (pointA.x + pointB.x) / 2.0;
        CGFloat midY = (pointA.y + pointB.y) / 2.0;
        // 获取缩放比例
        CGFloat scale = [self getRealScale:self.lastScale * gestureRecognizer.scale];
        // 获取中心点在画面中的比例
        CGFloat videoCenterX = self.zoomRect->origin.x + midX * self.zoomRect->size.width / self.playerView.bounds.size.width;
        CGFloat videoCenterY = self.zoomRect->origin.y + midY * self.zoomRect->size.height / self.playerView.bounds.size.height;
        // 计算显示范围
        self.zoomRect->origin.x += (self.zoomRect->size.width - 1.0 / scale) * videoCenterX;
        self.zoomRect->origin.y += (self.zoomRect->size.height - 1.0 / scale) * videoCenterY;
        self.zoomRect->size.width = 1.0 / scale;
        self.zoomRect->size.height = 1.0 / scale;
        // 修正显示范围
        [EZBusinessTool judgeRect:self.zoomRect];
        // 电子放大，双目设备第二个镜头streamId传1
        [self.player setDisplayRegionEx:self.zoomRect streamId:0];
    } else if (gestureRecognizer.state == UIGestureRecognizerStateEnded) {
        // 捏合手势结束后，记录最终的缩放比例
        CGFloat scale = [self getRealScale:self.lastScale * gestureRecognizer.scale];
        self.lastScale = scale;
    }
}

/**
 * 单指拖移手势
 */
- (void)panGestureRecognizer2:(UIPanGestureRecognizer *)gestureRecognizer {
    if (gestureRecognizer.state == UIGestureRecognizerStateBegan) {
        // 记录初始触摸点
        self.movePoint2 = [gestureRecognizer locationInView:self.secondPlayerView];
    } else if (gestureRecognizer.state == UIGestureRecognizerStateChanged || gestureRecognizer.state == UIGestureRecognizerStateEnded) {
        // 计算当前触摸点和初始触摸点的差值
        CGPoint currentTouchPoint = [gestureRecognizer locationInView:self.secondPlayerView];
        CGFloat deltaX = currentTouchPoint.x - self.movePoint2.x;
        CGFloat deltaY = currentTouchPoint.y - self.movePoint2.y;
        
        CGFloat videoDeltaX = -deltaX * self.zoomRect2->size.width / self.secondPlayerView.bounds.size.width;
        CGFloat videoDeltaY = -deltaY * self.zoomRect2->size.height / self.secondPlayerView.bounds.size.height;

        self.zoomRect2->origin.x += videoDeltaX;
        self.zoomRect2->origin.y += videoDeltaY;
        // 修正显示范围
        [EZBusinessTool judgeRect:self.zoomRect2];
        // 电子放大，双目设备第二个镜头streamId传1
        [self.player setDisplayRegionEx:self.zoomRect2 streamId:1];
        // 如果手势结束，重置初始触摸点
        self.movePoint2 = currentTouchPoint;
    }
}

/**
 * 双指捏合手势
 */
- (void)pinchGestureRecognizer2:(UIPinchGestureRecognizer *)gestureRecognizer {
    if (gestureRecognizer.state == UIGestureRecognizerStateChanged) {
        if (gestureRecognizer.numberOfTouches != 2) {
            return;
        }
        // 获取双指坐标
        CGPoint pointA = [gestureRecognizer locationOfTouch:0 inView:self.secondPlayerView];
        CGPoint pointB = [gestureRecognizer locationOfTouch:1 inView:self.secondPlayerView];
        // 获取双指中心点坐标
        CGFloat midX = (pointA.x + pointB.x) / 2.0;
        CGFloat midY = (pointA.y + pointB.y) / 2.0;
        // 获取缩放比例
        CGFloat scale = [self getRealScale:self.lastScale2 * gestureRecognizer.scale];
        // 获取中心点在画面中的比例
        CGFloat videoCenterX = self.zoomRect2->origin.x + midX * self.zoomRect2->size.width / self.secondPlayerView.bounds.size.width;
        CGFloat videoCenterY = self.zoomRect2->origin.y + midY * self.zoomRect2->size.height / self.secondPlayerView.bounds.size.height;
        // 计算显示范围
        self.zoomRect2->origin.x += (self.zoomRect2->size.width - 1.0 / scale) * videoCenterX;
        self.zoomRect2->origin.y += (self.zoomRect2->size.height - 1.0 / scale) * videoCenterY;
        self.zoomRect2->size.width = 1.0 / scale;
        self.zoomRect2->size.height = 1.0 / scale;
        // 修正显示范围
        [EZBusinessTool judgeRect:self.zoomRect2];
        // 电子放大，双目设备第二个镜头streamId传1
        [self.player setDisplayRegionEx:self.zoomRect2 streamId:1];
    } else if (gestureRecognizer.state == UIGestureRecognizerStateEnded) {
        // 捏合手势结束后，记录最终的缩放比例
        CGFloat scale = [self getRealScale:self.lastScale2 * gestureRecognizer.scale];
        self.lastScale2 = scale;
    }
}

- (CGFloat)getRealScale:(CGFloat)scale {
    if (scale > MaximumZoomScale) {
        scale = MaximumZoomScale;
    } else if (scale < MinimumZoomScale) {
        scale = MinimumZoomScale;
    }
    return scale;
}

#pragma mark - private methods | 私有方法

/** 播放器初始化 */
- (void)setEZPlayer {
    // 1.创建回放播放器
    _player = [EZOPENSDK createPlayerWithDeviceSerial:_cameraInfo.deviceSerial cameraNo:_cameraInfo.cameraNo];
    // 2.配置播放器：设置代理，设置验证码，设置播放视图等配置
    _player.delegate = self;
    // 判断设备是否加密，加密就从demo的归档文件中获取设备验证码填入到播放器的验证码接口里
    // 本demo将验证码存储于归档文件中，可根据自己业务需求自行更改
    if (self.deviceInfo.isEncrypt) {
        _verifyCode = [[GlobalKit shareKit].deviceVerifyCodeBySerial objectForKey:_cameraInfo.deviceSerial];
        [_player setPlayVerifyCode:_verifyCode];
    }
    [_player setPlayerView:_playerView];// 设置playView
    if (self.isMultiChannelDevice) {// 双目设备设置第二个playerView
        [_player setPlayerView:_secondPlayerView streamId:1];// 设置playView
    }
}

/** 根据录像类别开始回放 */
- (void)startPlaybackByRecordCategory {
    if (_recordCategory == EZRecordCategoryDevice) {
        // 加载SD卡回放特殊配置，一般开发者可不用配置，按需设置
        [EZStreamConfigLoader loadPlaybackSpecialConfigToPlayer:_player];
        [_player startPlaybackFromDevice:_deviceRecord];
    } else {
        [_player startPlaybackFromCloud:_cloudRecord];
    }
}

/** 开始回放 */
- (void)doPlayback {
    if (_recordCategory == EZRecordCategoryDevice) {
        _deviceRecord = self.selectedIndexPath ? [_records dd_objectAtIndex:self.selectedIndexPath.row] : [_records firstObject];
    } else {
        _cloudRecord = self.selectedIndexPath ? [_records dd_objectAtIndex:self.selectedIndexPath.row] : [_records firstObject];
    }
    [self startPlaybackByRecordCategory];
    
    [self setPlaybackSliderUI];
    self.messageLabel.hidden = YES;
    [self.loadingView startSquareClcokwiseAnimation];
    [self.rateBtn setTitle:@"1倍" forState:UIControlStateNormal];
    
    _playbackStatus = EZPlaybackStatusPlaying;
}

- (void)invalidateTimer {
    if (self.playbackTimer) {
        [self.playbackTimer invalidate];
        self.playbackTimer = nil;
    }
}

- (void)repeatDelay {
    self.selectedCell = NO;
}

/**
 * 设置回放工具栏按钮的可用状态
 * @param enabled YES-可点击，NO-不可点击
 */
- (void)setPlaybackControlButtonsEnabled:(BOOL)enabled {
    self.playButton.enabled = enabled;
    self.voiceButton.enabled = enabled;
    self.downloadButton.enabled = enabled;
    self.rateBtn.enabled = enabled;
    // 倍数按钮文字颜色和边框颜色跟随状态变化
    UIColor *rateBtnColor = enabled ? [UIColor colorWithWhite:1.0 alpha:0.85] : [UIColor colorWithWhite:1.0 alpha:0.3];
    [self.rateBtn setTitleColor:rateBtnColor forState:UIControlStateNormal];
    self.rateBtn.layer.borderColor = rateBtnColor.CGColor;
}

/** 设置取流方式*/
- (void)showStreamFetchType {    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        int type = [self.player getStreamFetchType];
        NSLog(@"getStreamFetchType: %d", type);
        if (type >= 0) {
            dispatch_async(dispatch_get_main_queue(), ^{
                self.streamTypeLabel.hidden = NO;
                self.streamTypeLabel.text = [EZBusinessTool getStreamType:type];
            });
        }
    });
}

/** 云录像倍速选择底部弹框 */
- (void)showCloudRateActionSheet {
    NSArray<NSNumber *> *rates = [[EZBusinessTool getCloudPlaybackRates] reverseObjectEnumerator].allObjects;
    NSArray<NSString *> *rateStrings = [[EZBusinessTool getCloudPlaybackRateStrings] reverseObjectEnumerator].allObjects;
    [self showActionSheetWithTitle:NSLocalizedString(@"playback_rate_select", @"请选择回放倍速") message:nil appearanceProcess:^(JXTAlertController * _Nonnull alertMaker) {
        for (NSString *rateStr in rateStrings) {
            alertMaker.addActionDefaultTitle(rateStr);
        }
        alertMaker.addActionCancelTitle(NSLocalizedString(@"cancel", @"取消"));
    } actionsBlock:^(NSInteger buttonIndex, UIAlertAction * _Nonnull action, JXTAlertController * _Nonnull alertSelf) {
        if (buttonIndex < (NSInteger)rates.count) {
            EZPlaybackRate rate = (EZPlaybackRate)[rates[buttonIndex] integerValue];
            [self.player setPlaybackRate:rate mode:0];
            [self.rateBtn setTitle:rateStrings[buttonIndex] forState:UIControlStateNormal];
        }
    }];
}

/** SD卡录像倍速选择底部弹框 */
- (void)showSDCardRateActionSheet {
    int streamFetchType = [self.player getStreamFetchType];
    NSArray<NSNumber *> *rates = [[EZBusinessTool getSDCardPlaybackRates:self.deviceInfo cameraInfo:self.cameraInfo streamFetchType:streamFetchType] reverseObjectEnumerator].allObjects;
    NSArray<NSString *> *rateStrings = [[EZBusinessTool getSDCardPlaybackRateStrings:self.deviceInfo cameraInfo:self.cameraInfo streamFetchType:streamFetchType] reverseObjectEnumerator].allObjects;
    
    if (!rates || rates.count == 0) {
        [EZToast show:NSLocalizedString(@"device_playbackrate_not_support", @"该设备不支持回放倍率设置")];
        return;
    }
    
    [self showActionSheetWithTitle:NSLocalizedString(@"playback_rate_select", @"请选择回放倍速") message:nil appearanceProcess:^(JXTAlertController * _Nonnull alertMaker) {
        for (NSString *rateStr in rateStrings) {
            alertMaker.addActionDefaultTitle(rateStr);
        }
        alertMaker.addActionCancelTitle(NSLocalizedString(@"cancel", @"取消"));
    } actionsBlock:^(NSInteger buttonIndex, UIAlertAction * _Nonnull action, JXTAlertController * _Nonnull alertSelf) {
        if (buttonIndex < (NSInteger)rates.count) {
            EZPlaybackRate rate = (EZPlaybackRate)[rates[buttonIndex] integerValue];
            [self.player setPlaybackRate:rate mode:0];
            [self.rateBtn setTitle:rateStrings[buttonIndex] forState:UIControlStateNormal];
        }
    }];
}

/** 数据初始化 */
- (void)setData {
    if ([EZBusinessTool isHubDevice:self.deviceInfo.deviceType]) {// HUB设备，普通开发者无视此分支
        _cameraInfo = [[EZCameraInfo alloc] init];
        _cameraInfo.deviceSerial = _hubCoDevSerial;
        _cameraInfo.cameraNo = _cameraIndex;
    } else {// 非HUB设备
        // 没有通道信息，则取子设备信息；如果自己的设备没有挂载在网关下的话，不用考虑子设备
        NSArray *array = self.deviceInfo.cameraInfo ? self.deviceInfo.cameraInfo : self.deviceInfo.subDeviceInfo;
        _cameraInfo = [array dd_objectAtIndex:_cameraIndex];
        _isMultiChannelDevice = [EZBusinessTool isSupportMultiChannel:self.deviceInfo cameraInfo:self.cameraInfo];
    }
    
    self.rect = CGRectMake(0, 0, 1, 1);
    self.zoomRect = &(self->_rect);
    self.lastScale = 1;
    
    self.rect2 = CGRectMake(0, 0, 1, 1);
    self.zoomRect2 = &(self->_rect2);
    self.lastScale2 = 1;
}

#pragma mark - Get Methods

- (UIButton *)recordTypeButton {
    if (!_recordTypeButton) {
        _recordTypeButton = [[UIButton alloc] initNavigationButton:[UIImage systemImageNamed:@"slider.horizontal.3"]];
        [_recordTypeButton addTarget:self action:@selector(rightNavClicked) forControlEvents:UIControlEventTouchUpInside];
        _recordTypeButton.hidden = _isCloudRightNavHidden;
    }
    return _recordTypeButton;
}

- (NSArray<FTPopOverMenuModel *> *)recordTypeMenuArray {
    if (!_recordTypeMenuArray) {
        _recordTypeMenuArray = @[
            [[FTPopOverMenuModel alloc] initWithTitle:@"全部" image:@"" selected:YES],
            [[FTPopOverMenuModel alloc] initWithTitle:@"定时" image:@"" selected:NO],
            [[FTPopOverMenuModel alloc] initWithTitle:@"事件" image:@"" selected:NO],
        ];
    }
    return _recordTypeMenuArray;
}

- (NSDate *)beginTime {
    // iOS15.4 + 设置-通用-日期与时间-24小时制关闭情况下，需要设置NSDateFormatter的local为zh_CN，否则转换有问题。本demo已在分类里做了统一设置
    NSString *beginTimeString = [NSString stringWithFormat:@"%@ 00:00:00", [self.datePicker.date formattedDateWithFormat:yMdFormat]];
    _beginTime = [NSDate dateWithString:beginTimeString formatString:yMdHmsFormat];
    return _beginTime;
}

- (NSDate *)endTime {
    NSString *endTimeString = [NSString stringWithFormat:@"%@ 23:59:59", [self.datePicker.date formattedDateWithFormat:yMdFormat]];
    _endTime = [NSDate dateWithString:endTimeString formatString:yMdHmsFormat];
    return _endTime;
}

#pragma mark - UI

- (void)setUI {
#ifdef EZVIZ_GLOBAL_DEMO
    _isCloudRightNavHidden = NO;
#else
    _isCloudRightNavHidden = YES;
#endif
    self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:self.recordTypeButton];

    if (!_records)
        _records = [NSMutableArray new];
    
    _isOpenSound = YES;
    self.isAutorotate = YES;
    self.largeTitleLabel.text = self.deviceInfo.deviceName;
    self.largeTitleLabel.hidden = YES;
    _isShowToolbox = YES;
    
    DDCollectionViewFlowLayout *flowLayout = [[DDCollectionViewFlowLayout alloc] init];
    flowLayout.delegate = self;
    [self.playbackList setCollectionViewLayout:flowLayout];
    
    [self addHeaderRefresh];
    
    // loadingView创建
    if (!_loadingView) {
        _loadingView = [[HIKLoadView alloc] initWithHIKLoadViewStyle:HIKLoadViewStyleSqureClockWise];
    }
    [self.view insertSubview:_loadingView aboveSubview:self.secondPlayerView];
    [_loadingView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.width.height.mas_equalTo(@14);
        make.centerX.mas_equalTo(self.playerView.mas_centerX);
        make.centerY.mas_equalTo(self.playerView.mas_centerY);
    }];
    [_loadingView stopSquareClockwiseAnimation];
    // 进程条设置
    [self.duringSlider setThumbImage:[UIImage imageNamed:@"slider"] forState:UIControlStateNormal];
    [self.duringSlider setThumbImage:[UIImage imageNamed:@"slider_sel"] forState:UIControlStateHighlighted];

    self.cloudButton.selected = YES;
    self.largeBackButton.hidden = YES;
    self.rateBtn.layer.borderColor = [UIColor whiteColor].CGColor;
    // 顶部日期控件设置
    self.dateTextField.inputView = self.datePicker;
    self.dateTextField.inputAccessoryView = self.dateToolbar;
    NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
    dateFormatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"zh_CN"];
    dateFormatter.dateFormat = @"MM-dd ▽";
    [self.dateButton setTitle:[dateFormatter stringFromDate:self.datePicker.date] forState:UIControlStateNormal];
    dateFormatter.dateFormat = @"MM-dd △";
    [self.dateButton setTitle:[dateFormatter stringFromDate:self.datePicker.date] forState:UIControlStateSelected];
    EZWeak(self);
    [self.cloudAdImage addActionBlock:^(UIView * _Nullable view) {
        EZStrong(self);
        [strongself clickCloudAd];
    }];
    if (self.isMultiChannelDevice) {
        self.largeButton.hidden = YES;
        [self setConstraintForSecondPlayer];
    }
    // playerView添加捏合、拖移手势监听，做画面电子放大功能实现。(双目设备第二个镜头画面电子放大实现方式相同，setDisplayRegionEx:streamId:方法中的streamId传1即可)
    [self.playerView addGestureRecognizer:({
        UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panGestureRecognizer:)];
        panGesture;
    })];
    [self.playerView addGestureRecognizer:({
        UIPinchGestureRecognizer *gesure = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(pinchGestureRecognizer:)];
        gesure;
    })];
    if (self.isMultiChannelDevice) {
        [self.secondPlayerView addGestureRecognizer:({
            UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panGestureRecognizer2:)];
            panGesture;
        })];
        [self.secondPlayerView addGestureRecognizer:({
            UIPinchGestureRecognizer *gesure = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(pinchGestureRecognizer2:)];
            gesure;
        })];
    }
}

/** 双目设备调整约束 */
- (void)setConstraintForSecondPlayer {
    [self.loadingView mas_updateConstraints:^(MASConstraintMaker *make) {
        make.centerY.mas_equalTo(self.playerView.mas_bottom).offset(2.5);
    }];
    [self.messageLabel mas_updateConstraints:^(MASConstraintMaker *make) {
        make.centerY.mas_equalTo(self.playerView.mas_bottom).offset(2.5);
    }];
    self.secondPlayerView.hidden = NO;
    self.playerViewDividLine.hidden = NO;
    // 播放器工具条挪到第二个播放器的下方
    [self.view removeConstraint:self.playerToolboxTopConstraint];
    NSLayoutConstraint *topConstraint = [NSLayoutConstraint constraintWithItem:self.playerToolbox attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.secondPlayerView attribute:NSLayoutAttributeBottom multiplier:1.0 constant:0];
    [self.view addConstraint:topConstraint];
    // 调整与第一个播放窗口的间距
    self.secondPlayerViewTopMarginContraint.constant = 5;
}

/** 鱼眼设备专用设置 */
- (void)setFecUI {
    if ([FecViewLayoutHelper isFecDevice:self.deviceInfo]) {
        // 鱼眼设备显示"查看模式" & 调整画面比例为1:1
        [self.largeButton setImage:[UIImage imageNamed:@"view_type_gray"] forState:UIControlStateNormal];// 鱼眼设备不支持全屏，把图标改成查看模式图标
        [self.largeButton setImage:[UIImage imageNamed:@"view_type"] forState:UIControlStateHighlighted];
        self.fecPlaceType = EZFecPlaceTypeCeiling;// demo中默认顶装
        self.fecCorrectType = EZFecCorrectTypeFish;// demo中默认鱼眼（原始码流）
        self.fecCorrectTypeButtons = @[self.correct4PtzBtn, self.correct5PtzBtn, self.correctFull5PtzBtn,
                                  self.correctLatBtn, self.correctARCHorBtn, self.correctARCVerBtn,
                                  self.correctWideAngleBtn, self.correct180Btn, self.correct360Btn,
                                  self.correctCycBtn];
        // 先创建helper，ptz1PlayView创建的时候需要使用helper
        self.fecViewLayoutHelper = [[FecViewLayoutHelper alloc] init];
        self.fecViewLayoutHelper.scrollView = self.playerView;
        self.fecViewLayoutHelper.playerView = self.playerView;
        self.fecViewLayoutHelper.player = self.player;
        self.fecViewLayoutHelper.playerViewAspectContraint = self.playerViewAspectContraint;
        [self.fecViewLayoutHelper setPlayViewAspectRadioWith1V1];
        
        [self.view addSubview:self.fecViewLayoutHelper.ptz1PlayView];
        [self.view addSubview:self.fecViewLayoutHelper.ptz2PlayView];
        [self.view addSubview:self.fecViewLayoutHelper.ptz3PlayView];
        [self.view addSubview:self.fecViewLayoutHelper.ptz4PlayView];
        [self.view addSubview:self.fecViewLayoutHelper.ptz5PlayView];
        [self.view addSubview:self.fecViewLayoutHelper.ptz6PlayView];
    }
}

@end
