| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561 |
- //
- // JXPlayerViewController.m
- // AICity
- //
- // Created by TogetherWatch on 2025-10-13.
- //
- #import "JXPlayerViewController.h"
- #import "JXAPIService.h"
- #import "JXDrama.h"
- #import "JXEpisode.h"
- #import "JXInteraction.h"
- #import "JXAVPlayer.h"
- #import "JXMemberService.h"
- #import "JXMemberPromptViewController.h"
- @interface JXPlayerViewController () <JXAVPlayerDelegate, JXAVPlayerProgressDelegate>
- // 播放器
- @property (nonatomic, strong) JXAVPlayer *avPlayer;
- @property (nonatomic, strong) AVPlayerLayer *playerLayer;
- // UI元素
- @property (nonatomic, strong) UIView *titleBanner;
- @property (nonatomic, strong) UIView *interactionButtons;
- @property (nonatomic, strong) UIView *bottomOverlay;
- @property (nonatomic, strong) UIView *bottomNavigation;
- // UI自动隐藏
- @property (nonatomic, assign) BOOL isUIVisible;
- @property (nonatomic, strong) NSTimer *hideUITimer;
- // 数据
- @property (nonatomic, strong) JXDrama *drama;
- @property (nonatomic, strong) JXEpisode *episode;
- @property (nonatomic, strong) NSArray<JXEpisode *> *episodes;
- @property (nonatomic, strong) JXInteraction *interaction;
- // 会员服务
- @property (nonatomic, strong) JXMemberService *memberService;
- @end
- @implementation JXPlayerViewController
- - (void)viewDidLoad {
- [super viewDidLoad];
-
- self.view.backgroundColor = [UIColor blackColor];
- self.isUIVisible = YES;
-
- [self setupPlayer];
- [self setupUI];
- [self setupGestureRecognizers];
- [self loadData];
- }
- - (void)setupPlayer {
- // 创建JXAVPlayer
- self.avPlayer = [[JXAVPlayer alloc] init];
- self.avPlayer.delegate = self;
- self.avPlayer.progressDelegate = self;
-
- // 创建AVPlayerLayer - 高度要在底部页签上方结束
- // 底部页签高度: 44
- CGFloat bottomTabBarHeight = 44;
- CGRect playerFrame = self.view.bounds;
- playerFrame.size.height -= bottomTabBarHeight;
-
- self.playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.avPlayer.player];
- self.playerLayer.frame = playerFrame;
- self.playerLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
- [self.view.layer insertSublayer:self.playerLayer atIndex:0];
-
- NSLog(@"🎬 AVPlayerLayer 创建完成, frame: %@, 底部页签高度: %.0f", NSStringFromCGRect(self.playerLayer.frame), bottomTabBarHeight);
- }
- - (void)setupGestureRecognizers {
- // 添加点击手势,用于显示/隐藏UI
- UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)];
- [self.view addGestureRecognizer:tapGesture];
- }
- - (void)handleTap:(UITapGestureRecognizer *)gesture {
- [self toggleUIVisibility];
- }
- - (void)setupUI {
- // 创建UI元素(参考quickstart.md中的实现)
- [self createTitleBanner];
- [self createInteractionButtons];
- [self createBottomOverlay];
- [self createBottomNavigation];
-
- // 设置自动隐藏
- [self setupAutoHideUI];
- }
- - (void)createTitleBanner {
- self.titleBanner = [[UIView alloc] init];
- self.titleBanner.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.8];
- self.titleBanner.layer.cornerRadius = 20;
-
- // TODO: 添加货币图标和剧集标题
- // 参考quickstart.md中的详细实现
-
- [self.view addSubview:self.titleBanner];
-
- self.titleBanner.translatesAutoresizingMaskIntoConstraints = NO;
- [NSLayoutConstraint activateConstraints:@[
- [self.titleBanner.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor],
- [self.titleBanner.topAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor constant:50],
- [self.titleBanner.heightAnchor constraintEqualToConstant:44]
- ]];
- }
- - (void)createInteractionButtons {
- self.interactionButtons = [[UIView alloc] init];
-
- // TODO: 添加关注、点赞、评论、收藏、分享按钮
- // 参考quickstart.md中的详细实现
-
- [self.view addSubview:self.interactionButtons];
-
- self.interactionButtons.translatesAutoresizingMaskIntoConstraints = NO;
- [NSLayoutConstraint activateConstraints:@[
- [self.interactionButtons.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor constant:-16],
- [self.interactionButtons.centerYAnchor constraintEqualToAnchor:self.view.centerYAnchor]
- ]];
- }
- - (void)createBottomOverlay {
- self.bottomOverlay = [[UIView alloc] init];
- self.bottomOverlay.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.7];
-
- // TODO: 添加弹幕标识、标题、作者、集数、标签
- // 参考quickstart.md中的详细实现
-
- [self.view addSubview:self.bottomOverlay];
-
- self.bottomOverlay.translatesAutoresizingMaskIntoConstraints = NO;
- [NSLayoutConstraint activateConstraints:@[
- [self.bottomOverlay.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
- [self.bottomOverlay.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
- [self.bottomOverlay.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor constant:-44],
- [self.bottomOverlay.heightAnchor constraintEqualToConstant:150]
- ]];
- }
- - (void)createBottomNavigation {
- self.bottomNavigation = [[UIView alloc] init];
- self.bottomNavigation.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.5];
-
- // TODO: 添加分类、更新状态、进度指示器
- // 参考quickstart.md中的详细实现
-
- [self.view addSubview:self.bottomNavigation];
-
- self.bottomNavigation.translatesAutoresizingMaskIntoConstraints = NO;
- [NSLayoutConstraint activateConstraints:@[
- [self.bottomNavigation.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
- [self.bottomNavigation.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
- [self.bottomNavigation.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor],
- [self.bottomNavigation.heightAnchor constraintEqualToConstant:44]
- ]];
- }
- - (void)setupAutoHideUI {
- // 3秒后自动隐藏UI
- [self scheduleAutoHideUI];
- }
- #pragma mark - UI Auto Hide
- - (void)toggleUIVisibility {
- if (self.isUIVisible) {
- [self hideUIElements];
- } else {
- [self showUIElements];
- [self scheduleAutoHideUI];
- }
- }
- - (void)scheduleAutoHideUI {
- [self cancelAutoHideUI];
- self.hideUITimer = [NSTimer scheduledTimerWithTimeInterval:3.0
- target:self
- selector:@selector(autoHideUI)
- userInfo:nil
- repeats:NO];
- }
- - (void)cancelAutoHideUI {
- [self.hideUITimer invalidate];
- self.hideUITimer = nil;
- }
- - (void)autoHideUI {
- [self hideUIElements];
- }
- - (void)hideUIElements {
- self.isUIVisible = NO;
- [UIView animateWithDuration:0.3 animations:^{
- self.titleBanner.alpha = 0;
- self.interactionButtons.alpha = 0;
- self.bottomOverlay.alpha = 0;
- self.bottomNavigation.alpha = 0;
- }];
- }
- - (void)showUIElements {
- self.isUIVisible = YES;
- [UIView animateWithDuration:0.3 animations:^{
- self.titleBanner.alpha = 1;
- self.interactionButtons.alpha = 1;
- self.bottomOverlay.alpha = 1;
- self.bottomNavigation.alpha = 1;
- }];
- }
- #pragma mark - Data Loading
- - (void)loadData {
- // 加载短剧详情
- [[JXAPIService sharedService] getDramaDetailWithDramaId:self.dramaId success:^(id responseObject) {
- NSDictionary *data = responseObject[@"data"];
- self.drama = [[JXDrama alloc] initWithDictionary:data[@"drama"]];
-
- NSArray *episodesData = data[@"episodes"];
- NSMutableArray *episodesArray = [NSMutableArray array];
- for (NSDictionary *episodeDict in episodesData) {
- JXEpisode *episode = [[JXEpisode alloc] initWithDictionary:episodeDict];
- [episodesArray addObject:episode];
- }
- self.episodes = episodesArray;
-
- // 找到当前要播放的剧集
- for (JXEpisode *ep in self.episodes) {
- if ([ep.jxEpisodeId isEqualToString:self.episodeId]) {
- self.episode = ep;
- break;
- }
- }
-
- if (!self.episode && self.episodes.count > 0) {
- self.episode = self.episodes[0];
- }
-
- self.interaction = [[JXInteraction alloc] initWithDictionary:data[@"interaction"]];
-
- [self updateUIWithData];
- [self loadPlayURL];
- } failure:^(NSError *error) {
- NSLog(@"加载短剧详情失败: %@", error.localizedDescription);
- [self showErrorMessage:error.localizedDescription];
- }];
- }
- - (void)loadPlayURL {
- // 首先检查内容访问权限
- [self checkContentAccessBeforePlayWithCompletion:^(BOOL hasAccess, JXAccessCheckResult *result, NSError *error) {
- if (error) {
- // 权限检查失败,显示错误但允许播放(降级处理)
- NSLog(@"权限检查失败: %@", error.localizedDescription);
- [self proceedToLoadPlayURL];
- } else if (hasAccess) {
- // 有权限,继续播放
- [self proceedToLoadPlayURL];
- } else {
- // 无权限,显示会员提示页面
- [self showMemberPromptWithResult:result];
- }
- }];
- }
- - (void)proceedToLoadPlayURL {
- [[JXAPIService sharedService] getPlayURLWithEpisodeId:self.episode.jxEpisodeId success:^(id responseObject) {
- NSDictionary *data = responseObject[@"data"];
- NSString *playURL = data[@"playUrl"];
- NSTimeInterval startPosition = [data[@"position"] doubleValue] / 1000.0; // 转换为秒
-
- [self.avPlayer playVideoWithURL:playURL episodeId:self.episode.jxEpisodeId startPosition:startPosition];
- } failure:^(NSError *error) {
- NSLog(@"获取播放地址失败: %@", error.localizedDescription);
- [self showErrorMessage:@"获取播放地址失败"];
- }];
- }
- - (void)updateUIWithData {
- // TODO: 更新所有UI元素
- }
- #pragma mark - JXAVPlayerDelegate
- - (void)playerDidReady {
- NSLog(@"播放器准备就绪");
- }
- - (void)playerDidStartPlaying {
- NSLog(@"开始播放");
- [self scheduleAutoHideUI];
- }
- - (void)playerDidPause {
- NSLog(@"暂停播放");
- [self cancelAutoHideUI];
- }
- - (void)playerDidBuffering {
- NSLog(@"缓冲中...");
- }
- - (void)playerDidFinishPlaying {
- NSLog(@"播放结束");
- [self playNextEpisode];
- }
- - (void)playerDidFailWithError:(NSError *)error {
- NSLog(@"播放错误: %@", error.localizedDescription);
- [self showErrorMessage:error.localizedDescription];
- }
- #pragma mark - JXAVPlayerProgressDelegate
- - (void)playerDidUpdateProgress:(NSString *)episodeId
- positionMs:(NSTimeInterval)positionMs
- durationMs:(NSTimeInterval)durationMs
- progress:(CGFloat)progress
- isCompleted:(BOOL)isCompleted {
- // 上报播放进度到服务器
- [[JXAPIService sharedService] reportPlaybackProgressWithEpisodeId:episodeId
- position:(NSInteger)positionMs
- duration:(NSInteger)durationMs
- isCompleted:isCompleted
- success:^(id responseObject) {
- NSLog(@"进度上报成功: %.2f%%", progress * 100);
- } failure:^(NSError *error) {
- NSLog(@"进度上报失败: %@", error.localizedDescription);
- }];
- }
- #pragma mark - Playback Control
- - (void)playNextEpisode {
- // 找到下一集
- NSInteger currentIndex = [self.episodes indexOfObject:self.episode];
- if (currentIndex != NSNotFound && currentIndex < self.episodes.count - 1) {
- self.episode = self.episodes[currentIndex + 1];
- self.episodeId = self.episode.jxEpisodeId;
-
- [self loadPlayURL];
- [self showToast:[NSString stringWithFormat:@"正在播放: %@", self.episode.title]];
- } else {
- [self showToast:@"已是最后一集"];
- [self dismissViewControllerAnimated:YES completion:nil];
- }
- }
- #pragma mark - Interaction Actions
- - (void)toggleLike {
- [[JXAPIService sharedService] toggleLikeWithDramaId:self.dramaId
- episodeId:self.episodeId
- success:^(id responseObject) {
- NSDictionary *data = responseObject[@"data"];
- if (data && data[@"interaction"]) {
- self.interaction = [[JXInteraction alloc] initWithDictionary:data[@"interaction"]];
- [self updateInteractionUI];
- [self showToast:self.interaction.isLiked ? @"已点赞" : @"已取消点赞"];
- }
- } failure:^(NSError *error) {
- [self showToast:@"操作失败"];
- }];
- }
- - (void)toggleFavorite {
- [[JXAPIService sharedService] toggleFavoriteWithDramaId:self.dramaId
- success:^(id responseObject) {
- NSDictionary *data = responseObject[@"data"];
- if (data && data[@"interaction"]) {
- self.interaction = [[JXInteraction alloc] initWithDictionary:data[@"interaction"]];
- [self updateInteractionUI];
- [self showToast:self.interaction.isFavorited ? @"已收藏" : @"已取消收藏"];
- }
- } failure:^(NSError *error) {
- [self showToast:@"操作失败"];
- }];
- }
- - (void)toggleFollow {
- [[JXAPIService sharedService] toggleFollowWithDramaId:self.dramaId
- success:^(id responseObject) {
- NSDictionary *data = responseObject[@"data"];
- if (data && data[@"interaction"]) {
- self.interaction = [[JXInteraction alloc] initWithDictionary:data[@"interaction"]];
- [self updateInteractionUI];
- [self showToast:self.interaction.isFollowed ? @"已关注" : @"已取消关注"];
- }
- } failure:^(NSError *error) {
- [self showToast:@"操作失败"];
- }];
- }
- - (void)showComments {
- // TODO: 实现评论功能
- [self showToast:@"评论功能开发中"];
- }
- - (void)shareContent {
- NSString *shareText = [NSString stringWithFormat:@"推荐一部好剧:%@ - %@",
- self.drama.title, self.episode.title];
- NSString *shareURL = [NSString stringWithFormat:@"https://app.example.com/juxing/drama/%@/episode/%@",
- self.dramaId, self.episodeId];
-
- UIActivityViewController *activityVC = [[UIActivityViewController alloc]
- initWithActivityItems:@[shareText, shareURL]
- applicationActivities:nil];
-
- [self presentViewController:activityVC animated:YES completion:nil];
- }
- - (void)updateInteractionUI {
- // TODO: 更新UI显示交互数据
- // 需要在创建交互按钮时保存按钮引用,然后在这里更新它们的状态和数字
- }
- #pragma mark - Helper Methods
- - (void)showErrorMessage:(NSString *)message {
- UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"错误"
- message:message
- preferredStyle:UIAlertControllerStyleAlert];
- [alert addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:nil]];
- [self presentViewController:alert animated:YES completion:nil];
- }
- - (void)showToast:(NSString *)message {
- // TODO: 实现Toast提示
- NSLog(@"Toast: %@", message);
- }
- #pragma mark - Member Access Control
- /**
- * 播放前检查内容访问权限
- */
- - (void)checkContentAccessBeforePlayWithCompletion:(void(^)(BOOL hasAccess, JXAccessCheckResult *result, NSError *error))completion {
- if (!self.memberService) {
- self.memberService = [JXMemberService sharedService];
- }
-
- // 确定所需权限级别
- NSInteger requiredLevel = [self determineRequiredAccessLevel];
-
- // 检查剧集访问权限
- [self.memberService checkEpisodeAccessWithEpisodeId:self.episode.jxEpisodeId
- requiredLevel:requiredLevel
- completion:^(JXAccessCheckResult * _Nullable result, NSError * _Nullable error) {
- dispatch_async(dispatch_get_main_queue(), ^{
- if (error) {
- completion(NO, nil, error);
- } else {
- completion(result.hasAccess, result, nil);
- }
- });
- }];
- }
- /**
- * 确定所需的访问权限级别
- */
- - (NSInteger)determineRequiredAccessLevel {
- // 根据剧集信息确定权限要求
- if (self.episode.isPaid) {
- // 付费内容,根据内容类型确定级别
- if ([self.episode.contentType isEqualToString:@"premium"]) {
- return 2; // 高级内容
- } else if ([self.episode.contentType isEqualToString:@"vip"]) {
- return 3; // VIP内容
- } else {
- return 1; // 普通付费内容
- }
- } else {
- return 0; // 免费内容
- }
- }
- /**
- * 显示会员提示页面
- */
- - (void)showMemberPromptWithResult:(JXAccessCheckResult *)result {
- // 创建会员提示视图控制器
- JXMemberPromptViewController *memberPromptVC = [[JXMemberPromptViewController alloc] init];
-
- // 设置数据
- memberPromptVC.episodeId = self.episode.jxEpisodeId;
- memberPromptVC.requiredLevel = result.requiredLevel;
- memberPromptVC.memberStatus = result.memberStatus;
-
- // 设置回调
- __weak typeof(self) weakSelf = self;
- memberPromptVC.onMemberPurchaseSuccess = ^{
- // 会员购买成功,重新检查权限并播放
- [weakSelf retryPlayAfterMemberPurchase];
- };
-
- memberPromptVC.onCancel = ^{
- // 用户取消,返回上一页面
- [weakSelf.navigationController popViewControllerAnimated:YES];
- };
-
- // 以模态方式显示
- memberPromptVC.modalPresentationStyle = UIModalPresentationFullScreen;
- [self presentViewController:memberPromptVC animated:YES completion:nil];
- }
- /**
- * 会员购买成功后重试播放
- */
- - (void)retryPlayAfterMemberPurchase {
- // 刷新会员状态并重新检查权限
- [self.memberService getCurrentMemberStatusWithForceRefresh:YES completion:^(JXMemberStatus * _Nullable memberStatus, NSError * _Nullable error) {
- if (error) {
- [self showErrorMessage:@"获取会员状态失败"];
- return;
- }
-
- // 重新检查权限
- [self checkContentAccessBeforePlayWithCompletion:^(BOOL hasAccess, JXAccessCheckResult *result, NSError *error) {
- if (error) {
- [self showErrorMessage:@"权限检查失败"];
- return;
- }
-
- if (hasAccess) {
- // 现在有权限了,开始播放
- [self proceedToLoadPlayURL];
- } else {
- // 仍然没有权限
- [self showErrorMessage:@"会员权限不足,请升级会员"];
- }
- }];
- }];
- }
- #pragma mark - Lifecycle
- - (void)viewWillDisappear:(BOOL)animated {
- [super viewWillDisappear:animated];
- [self.avPlayer pause];
- [self.avPlayer reportCurrentProgress];
- }
- - (void)dealloc {
- [self cancelAutoHideUI];
- [self.avPlayer releasePlayer];
- }
- @end
|