JXPlayerViewController.m 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561
  1. //
  2. // JXPlayerViewController.m
  3. // AICity
  4. //
  5. // Created by TogetherWatch on 2025-10-13.
  6. //
  7. #import "JXPlayerViewController.h"
  8. #import "JXAPIService.h"
  9. #import "JXDrama.h"
  10. #import "JXEpisode.h"
  11. #import "JXInteraction.h"
  12. #import "JXAVPlayer.h"
  13. #import "JXMemberService.h"
  14. #import "JXMemberPromptViewController.h"
  15. @interface JXPlayerViewController () <JXAVPlayerDelegate, JXAVPlayerProgressDelegate>
  16. // 播放器
  17. @property (nonatomic, strong) JXAVPlayer *avPlayer;
  18. @property (nonatomic, strong) AVPlayerLayer *playerLayer;
  19. // UI元素
  20. @property (nonatomic, strong) UIView *titleBanner;
  21. @property (nonatomic, strong) UIView *interactionButtons;
  22. @property (nonatomic, strong) UIView *bottomOverlay;
  23. @property (nonatomic, strong) UIView *bottomNavigation;
  24. // UI自动隐藏
  25. @property (nonatomic, assign) BOOL isUIVisible;
  26. @property (nonatomic, strong) NSTimer *hideUITimer;
  27. // 数据
  28. @property (nonatomic, strong) JXDrama *drama;
  29. @property (nonatomic, strong) JXEpisode *episode;
  30. @property (nonatomic, strong) NSArray<JXEpisode *> *episodes;
  31. @property (nonatomic, strong) JXInteraction *interaction;
  32. // 会员服务
  33. @property (nonatomic, strong) JXMemberService *memberService;
  34. @end
  35. @implementation JXPlayerViewController
  36. - (void)viewDidLoad {
  37. [super viewDidLoad];
  38. self.view.backgroundColor = [UIColor blackColor];
  39. self.isUIVisible = YES;
  40. [self setupPlayer];
  41. [self setupUI];
  42. [self setupGestureRecognizers];
  43. [self loadData];
  44. }
  45. - (void)setupPlayer {
  46. // 创建JXAVPlayer
  47. self.avPlayer = [[JXAVPlayer alloc] init];
  48. self.avPlayer.delegate = self;
  49. self.avPlayer.progressDelegate = self;
  50. // 创建AVPlayerLayer - 高度要在底部页签上方结束
  51. // 底部页签高度: 44
  52. CGFloat bottomTabBarHeight = 44;
  53. CGRect playerFrame = self.view.bounds;
  54. playerFrame.size.height -= bottomTabBarHeight;
  55. self.playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.avPlayer.player];
  56. self.playerLayer.frame = playerFrame;
  57. self.playerLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
  58. [self.view.layer insertSublayer:self.playerLayer atIndex:0];
  59. NSLog(@"🎬 AVPlayerLayer 创建完成, frame: %@, 底部页签高度: %.0f", NSStringFromCGRect(self.playerLayer.frame), bottomTabBarHeight);
  60. }
  61. - (void)setupGestureRecognizers {
  62. // 添加点击手势,用于显示/隐藏UI
  63. UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)];
  64. [self.view addGestureRecognizer:tapGesture];
  65. }
  66. - (void)handleTap:(UITapGestureRecognizer *)gesture {
  67. [self toggleUIVisibility];
  68. }
  69. - (void)setupUI {
  70. // 创建UI元素(参考quickstart.md中的实现)
  71. [self createTitleBanner];
  72. [self createInteractionButtons];
  73. [self createBottomOverlay];
  74. [self createBottomNavigation];
  75. // 设置自动隐藏
  76. [self setupAutoHideUI];
  77. }
  78. - (void)createTitleBanner {
  79. self.titleBanner = [[UIView alloc] init];
  80. self.titleBanner.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.8];
  81. self.titleBanner.layer.cornerRadius = 20;
  82. // TODO: 添加货币图标和剧集标题
  83. // 参考quickstart.md中的详细实现
  84. [self.view addSubview:self.titleBanner];
  85. self.titleBanner.translatesAutoresizingMaskIntoConstraints = NO;
  86. [NSLayoutConstraint activateConstraints:@[
  87. [self.titleBanner.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor],
  88. [self.titleBanner.topAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor constant:50],
  89. [self.titleBanner.heightAnchor constraintEqualToConstant:44]
  90. ]];
  91. }
  92. - (void)createInteractionButtons {
  93. self.interactionButtons = [[UIView alloc] init];
  94. // TODO: 添加关注、点赞、评论、收藏、分享按钮
  95. // 参考quickstart.md中的详细实现
  96. [self.view addSubview:self.interactionButtons];
  97. self.interactionButtons.translatesAutoresizingMaskIntoConstraints = NO;
  98. [NSLayoutConstraint activateConstraints:@[
  99. [self.interactionButtons.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor constant:-16],
  100. [self.interactionButtons.centerYAnchor constraintEqualToAnchor:self.view.centerYAnchor]
  101. ]];
  102. }
  103. - (void)createBottomOverlay {
  104. self.bottomOverlay = [[UIView alloc] init];
  105. self.bottomOverlay.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.7];
  106. // TODO: 添加弹幕标识、标题、作者、集数、标签
  107. // 参考quickstart.md中的详细实现
  108. [self.view addSubview:self.bottomOverlay];
  109. self.bottomOverlay.translatesAutoresizingMaskIntoConstraints = NO;
  110. [NSLayoutConstraint activateConstraints:@[
  111. [self.bottomOverlay.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
  112. [self.bottomOverlay.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
  113. [self.bottomOverlay.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor constant:-44],
  114. [self.bottomOverlay.heightAnchor constraintEqualToConstant:150]
  115. ]];
  116. }
  117. - (void)createBottomNavigation {
  118. self.bottomNavigation = [[UIView alloc] init];
  119. self.bottomNavigation.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.5];
  120. // TODO: 添加分类、更新状态、进度指示器
  121. // 参考quickstart.md中的详细实现
  122. [self.view addSubview:self.bottomNavigation];
  123. self.bottomNavigation.translatesAutoresizingMaskIntoConstraints = NO;
  124. [NSLayoutConstraint activateConstraints:@[
  125. [self.bottomNavigation.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
  126. [self.bottomNavigation.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
  127. [self.bottomNavigation.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor],
  128. [self.bottomNavigation.heightAnchor constraintEqualToConstant:44]
  129. ]];
  130. }
  131. - (void)setupAutoHideUI {
  132. // 3秒后自动隐藏UI
  133. [self scheduleAutoHideUI];
  134. }
  135. #pragma mark - UI Auto Hide
  136. - (void)toggleUIVisibility {
  137. if (self.isUIVisible) {
  138. [self hideUIElements];
  139. } else {
  140. [self showUIElements];
  141. [self scheduleAutoHideUI];
  142. }
  143. }
  144. - (void)scheduleAutoHideUI {
  145. [self cancelAutoHideUI];
  146. self.hideUITimer = [NSTimer scheduledTimerWithTimeInterval:3.0
  147. target:self
  148. selector:@selector(autoHideUI)
  149. userInfo:nil
  150. repeats:NO];
  151. }
  152. - (void)cancelAutoHideUI {
  153. [self.hideUITimer invalidate];
  154. self.hideUITimer = nil;
  155. }
  156. - (void)autoHideUI {
  157. [self hideUIElements];
  158. }
  159. - (void)hideUIElements {
  160. self.isUIVisible = NO;
  161. [UIView animateWithDuration:0.3 animations:^{
  162. self.titleBanner.alpha = 0;
  163. self.interactionButtons.alpha = 0;
  164. self.bottomOverlay.alpha = 0;
  165. self.bottomNavigation.alpha = 0;
  166. }];
  167. }
  168. - (void)showUIElements {
  169. self.isUIVisible = YES;
  170. [UIView animateWithDuration:0.3 animations:^{
  171. self.titleBanner.alpha = 1;
  172. self.interactionButtons.alpha = 1;
  173. self.bottomOverlay.alpha = 1;
  174. self.bottomNavigation.alpha = 1;
  175. }];
  176. }
  177. #pragma mark - Data Loading
  178. - (void)loadData {
  179. // 加载短剧详情
  180. [[JXAPIService sharedService] getDramaDetailWithDramaId:self.dramaId success:^(id responseObject) {
  181. NSDictionary *data = responseObject[@"data"];
  182. self.drama = [[JXDrama alloc] initWithDictionary:data[@"drama"]];
  183. NSArray *episodesData = data[@"episodes"];
  184. NSMutableArray *episodesArray = [NSMutableArray array];
  185. for (NSDictionary *episodeDict in episodesData) {
  186. JXEpisode *episode = [[JXEpisode alloc] initWithDictionary:episodeDict];
  187. [episodesArray addObject:episode];
  188. }
  189. self.episodes = episodesArray;
  190. // 找到当前要播放的剧集
  191. for (JXEpisode *ep in self.episodes) {
  192. if ([ep.jxEpisodeId isEqualToString:self.episodeId]) {
  193. self.episode = ep;
  194. break;
  195. }
  196. }
  197. if (!self.episode && self.episodes.count > 0) {
  198. self.episode = self.episodes[0];
  199. }
  200. self.interaction = [[JXInteraction alloc] initWithDictionary:data[@"interaction"]];
  201. [self updateUIWithData];
  202. [self loadPlayURL];
  203. } failure:^(NSError *error) {
  204. NSLog(@"加载短剧详情失败: %@", error.localizedDescription);
  205. [self showErrorMessage:error.localizedDescription];
  206. }];
  207. }
  208. - (void)loadPlayURL {
  209. // 首先检查内容访问权限
  210. [self checkContentAccessBeforePlayWithCompletion:^(BOOL hasAccess, JXAccessCheckResult *result, NSError *error) {
  211. if (error) {
  212. // 权限检查失败,显示错误但允许播放(降级处理)
  213. NSLog(@"权限检查失败: %@", error.localizedDescription);
  214. [self proceedToLoadPlayURL];
  215. } else if (hasAccess) {
  216. // 有权限,继续播放
  217. [self proceedToLoadPlayURL];
  218. } else {
  219. // 无权限,显示会员提示页面
  220. [self showMemberPromptWithResult:result];
  221. }
  222. }];
  223. }
  224. - (void)proceedToLoadPlayURL {
  225. [[JXAPIService sharedService] getPlayURLWithEpisodeId:self.episode.jxEpisodeId success:^(id responseObject) {
  226. NSDictionary *data = responseObject[@"data"];
  227. NSString *playURL = data[@"playUrl"];
  228. NSTimeInterval startPosition = [data[@"position"] doubleValue] / 1000.0; // 转换为秒
  229. [self.avPlayer playVideoWithURL:playURL episodeId:self.episode.jxEpisodeId startPosition:startPosition];
  230. } failure:^(NSError *error) {
  231. NSLog(@"获取播放地址失败: %@", error.localizedDescription);
  232. [self showErrorMessage:@"获取播放地址失败"];
  233. }];
  234. }
  235. - (void)updateUIWithData {
  236. // TODO: 更新所有UI元素
  237. }
  238. #pragma mark - JXAVPlayerDelegate
  239. - (void)playerDidReady {
  240. NSLog(@"播放器准备就绪");
  241. }
  242. - (void)playerDidStartPlaying {
  243. NSLog(@"开始播放");
  244. [self scheduleAutoHideUI];
  245. }
  246. - (void)playerDidPause {
  247. NSLog(@"暂停播放");
  248. [self cancelAutoHideUI];
  249. }
  250. - (void)playerDidBuffering {
  251. NSLog(@"缓冲中...");
  252. }
  253. - (void)playerDidFinishPlaying {
  254. NSLog(@"播放结束");
  255. [self playNextEpisode];
  256. }
  257. - (void)playerDidFailWithError:(NSError *)error {
  258. NSLog(@"播放错误: %@", error.localizedDescription);
  259. [self showErrorMessage:error.localizedDescription];
  260. }
  261. #pragma mark - JXAVPlayerProgressDelegate
  262. - (void)playerDidUpdateProgress:(NSString *)episodeId
  263. positionMs:(NSTimeInterval)positionMs
  264. durationMs:(NSTimeInterval)durationMs
  265. progress:(CGFloat)progress
  266. isCompleted:(BOOL)isCompleted {
  267. // 上报播放进度到服务器
  268. [[JXAPIService sharedService] reportPlaybackProgressWithEpisodeId:episodeId
  269. position:(NSInteger)positionMs
  270. duration:(NSInteger)durationMs
  271. isCompleted:isCompleted
  272. success:^(id responseObject) {
  273. NSLog(@"进度上报成功: %.2f%%", progress * 100);
  274. } failure:^(NSError *error) {
  275. NSLog(@"进度上报失败: %@", error.localizedDescription);
  276. }];
  277. }
  278. #pragma mark - Playback Control
  279. - (void)playNextEpisode {
  280. // 找到下一集
  281. NSInteger currentIndex = [self.episodes indexOfObject:self.episode];
  282. if (currentIndex != NSNotFound && currentIndex < self.episodes.count - 1) {
  283. self.episode = self.episodes[currentIndex + 1];
  284. self.episodeId = self.episode.jxEpisodeId;
  285. [self loadPlayURL];
  286. [self showToast:[NSString stringWithFormat:@"正在播放: %@", self.episode.title]];
  287. } else {
  288. [self showToast:@"已是最后一集"];
  289. [self dismissViewControllerAnimated:YES completion:nil];
  290. }
  291. }
  292. #pragma mark - Interaction Actions
  293. - (void)toggleLike {
  294. [[JXAPIService sharedService] toggleLikeWithDramaId:self.dramaId
  295. episodeId:self.episodeId
  296. success:^(id responseObject) {
  297. NSDictionary *data = responseObject[@"data"];
  298. if (data && data[@"interaction"]) {
  299. self.interaction = [[JXInteraction alloc] initWithDictionary:data[@"interaction"]];
  300. [self updateInteractionUI];
  301. [self showToast:self.interaction.isLiked ? @"已点赞" : @"已取消点赞"];
  302. }
  303. } failure:^(NSError *error) {
  304. [self showToast:@"操作失败"];
  305. }];
  306. }
  307. - (void)toggleFavorite {
  308. [[JXAPIService sharedService] toggleFavoriteWithDramaId:self.dramaId
  309. success:^(id responseObject) {
  310. NSDictionary *data = responseObject[@"data"];
  311. if (data && data[@"interaction"]) {
  312. self.interaction = [[JXInteraction alloc] initWithDictionary:data[@"interaction"]];
  313. [self updateInteractionUI];
  314. [self showToast:self.interaction.isFavorited ? @"已收藏" : @"已取消收藏"];
  315. }
  316. } failure:^(NSError *error) {
  317. [self showToast:@"操作失败"];
  318. }];
  319. }
  320. - (void)toggleFollow {
  321. [[JXAPIService sharedService] toggleFollowWithDramaId:self.dramaId
  322. success:^(id responseObject) {
  323. NSDictionary *data = responseObject[@"data"];
  324. if (data && data[@"interaction"]) {
  325. self.interaction = [[JXInteraction alloc] initWithDictionary:data[@"interaction"]];
  326. [self updateInteractionUI];
  327. [self showToast:self.interaction.isFollowed ? @"已关注" : @"已取消关注"];
  328. }
  329. } failure:^(NSError *error) {
  330. [self showToast:@"操作失败"];
  331. }];
  332. }
  333. - (void)showComments {
  334. // TODO: 实现评论功能
  335. [self showToast:@"评论功能开发中"];
  336. }
  337. - (void)shareContent {
  338. NSString *shareText = [NSString stringWithFormat:@"推荐一部好剧:%@ - %@",
  339. self.drama.title, self.episode.title];
  340. NSString *shareURL = [NSString stringWithFormat:@"https://app.example.com/juxing/drama/%@/episode/%@",
  341. self.dramaId, self.episodeId];
  342. UIActivityViewController *activityVC = [[UIActivityViewController alloc]
  343. initWithActivityItems:@[shareText, shareURL]
  344. applicationActivities:nil];
  345. [self presentViewController:activityVC animated:YES completion:nil];
  346. }
  347. - (void)updateInteractionUI {
  348. // TODO: 更新UI显示交互数据
  349. // 需要在创建交互按钮时保存按钮引用,然后在这里更新它们的状态和数字
  350. }
  351. #pragma mark - Helper Methods
  352. - (void)showErrorMessage:(NSString *)message {
  353. UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"错误"
  354. message:message
  355. preferredStyle:UIAlertControllerStyleAlert];
  356. [alert addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:nil]];
  357. [self presentViewController:alert animated:YES completion:nil];
  358. }
  359. - (void)showToast:(NSString *)message {
  360. // TODO: 实现Toast提示
  361. NSLog(@"Toast: %@", message);
  362. }
  363. #pragma mark - Member Access Control
  364. /**
  365. * 播放前检查内容访问权限
  366. */
  367. - (void)checkContentAccessBeforePlayWithCompletion:(void(^)(BOOL hasAccess, JXAccessCheckResult *result, NSError *error))completion {
  368. if (!self.memberService) {
  369. self.memberService = [JXMemberService sharedService];
  370. }
  371. // 确定所需权限级别
  372. NSInteger requiredLevel = [self determineRequiredAccessLevel];
  373. // 检查剧集访问权限
  374. [self.memberService checkEpisodeAccessWithEpisodeId:self.episode.jxEpisodeId
  375. requiredLevel:requiredLevel
  376. completion:^(JXAccessCheckResult * _Nullable result, NSError * _Nullable error) {
  377. dispatch_async(dispatch_get_main_queue(), ^{
  378. if (error) {
  379. completion(NO, nil, error);
  380. } else {
  381. completion(result.hasAccess, result, nil);
  382. }
  383. });
  384. }];
  385. }
  386. /**
  387. * 确定所需的访问权限级别
  388. */
  389. - (NSInteger)determineRequiredAccessLevel {
  390. // 根据剧集信息确定权限要求
  391. if (self.episode.isPaid) {
  392. // 付费内容,根据内容类型确定级别
  393. if ([self.episode.contentType isEqualToString:@"premium"]) {
  394. return 2; // 高级内容
  395. } else if ([self.episode.contentType isEqualToString:@"vip"]) {
  396. return 3; // VIP内容
  397. } else {
  398. return 1; // 普通付费内容
  399. }
  400. } else {
  401. return 0; // 免费内容
  402. }
  403. }
  404. /**
  405. * 显示会员提示页面
  406. */
  407. - (void)showMemberPromptWithResult:(JXAccessCheckResult *)result {
  408. // 创建会员提示视图控制器
  409. JXMemberPromptViewController *memberPromptVC = [[JXMemberPromptViewController alloc] init];
  410. // 设置数据
  411. memberPromptVC.episodeId = self.episode.jxEpisodeId;
  412. memberPromptVC.requiredLevel = result.requiredLevel;
  413. memberPromptVC.memberStatus = result.memberStatus;
  414. // 设置回调
  415. __weak typeof(self) weakSelf = self;
  416. memberPromptVC.onMemberPurchaseSuccess = ^{
  417. // 会员购买成功,重新检查权限并播放
  418. [weakSelf retryPlayAfterMemberPurchase];
  419. };
  420. memberPromptVC.onCancel = ^{
  421. // 用户取消,返回上一页面
  422. [weakSelf.navigationController popViewControllerAnimated:YES];
  423. };
  424. // 以模态方式显示
  425. memberPromptVC.modalPresentationStyle = UIModalPresentationFullScreen;
  426. [self presentViewController:memberPromptVC animated:YES completion:nil];
  427. }
  428. /**
  429. * 会员购买成功后重试播放
  430. */
  431. - (void)retryPlayAfterMemberPurchase {
  432. // 刷新会员状态并重新检查权限
  433. [self.memberService getCurrentMemberStatusWithForceRefresh:YES completion:^(JXMemberStatus * _Nullable memberStatus, NSError * _Nullable error) {
  434. if (error) {
  435. [self showErrorMessage:@"获取会员状态失败"];
  436. return;
  437. }
  438. // 重新检查权限
  439. [self checkContentAccessBeforePlayWithCompletion:^(BOOL hasAccess, JXAccessCheckResult *result, NSError *error) {
  440. if (error) {
  441. [self showErrorMessage:@"权限检查失败"];
  442. return;
  443. }
  444. if (hasAccess) {
  445. // 现在有权限了,开始播放
  446. [self proceedToLoadPlayURL];
  447. } else {
  448. // 仍然没有权限
  449. [self showErrorMessage:@"会员权限不足,请升级会员"];
  450. }
  451. }];
  452. }];
  453. }
  454. #pragma mark - Lifecycle
  455. - (void)viewWillDisappear:(BOOL)animated {
  456. [super viewWillDisappear:animated];
  457. [self.avPlayer pause];
  458. [self.avPlayer reportCurrentProgress];
  459. }
  460. - (void)dealloc {
  461. [self cancelAutoHideUI];
  462. [self.avPlayer releasePlayer];
  463. }
  464. @end