From 5a80e6693f74ae461a30d1b8ddd43f5081bd4d35 Mon Sep 17 00:00:00 2001 From: Zhou Weidong <502333560@qq.com> Date: Fri, 23 Oct 2026 12:34:13 +0800 Subject: [PATCH] =?UTF-8?q?feat(playback):=20=E6=92=AD=E6=94=BE=E7=A5=A8?= =?UTF-8?q?=E6=8D=AE=E8=BF=94=E5=9B=9E=E5=90=84=E6=B8=85=E6=99=B0=E5=BA=A6?= =?UTF-8?q?=E5=9C=B0=E5=9D=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/service/PlaybackServiceTest.java | 25 +++++++++++++++++++ .../playback/service/PlaybackService.java | 25 +++++++++++++++++++ .../fruitshorts/playback/vo/PlayRespVO.java | 2 ++ .../fruitshorts/playback/vo/PlayTrackVO.java | 16 ++++++++++++ 4 files changed, 68 insertions(+) create mode 100644 fruitshorts-module-playback/src/main/java/com/fruitshorts/playback/vo/PlayTrackVO.java diff --git a/fruitshorts-app-api/src/test/java/com/fruitshorts/app/service/PlaybackServiceTest.java b/fruitshorts-app-api/src/test/java/com/fruitshorts/app/service/PlaybackServiceTest.java index b6762e9..08681d4 100644 --- a/fruitshorts-app-api/src/test/java/com/fruitshorts/app/service/PlaybackServiceTest.java +++ b/fruitshorts-app-api/src/test/java/com/fruitshorts/app/service/PlaybackServiceTest.java @@ -5,6 +5,7 @@ import com.fruitshorts.content.dal.entity.Drama; import com.fruitshorts.content.dal.entity.DramaEpisode; import com.fruitshorts.content.service.DramaService; import com.fruitshorts.content.service.EpisodeService; +import com.fruitshorts.media.dal.entity.MediaRendition; import com.fruitshorts.media.service.PlaybackSigner; import com.fruitshorts.media.service.ThumbnailUrlResolver; import com.fruitshorts.media.service.TranscodeService; @@ -147,6 +148,30 @@ class PlaybackServiceTest { verify(signer).sign("/files/episode/15/master.m3u8"); } + @Test + void should_sign_each_ready_rendition_when_episode_has_multiple_qualities() { + DramaEpisode episode = episode(16L, 1, 0, "/files/episode/origin.mp4"); + episode.setHlsUrl("/files/transcoded/16/master.m3u8"); + given(episodeService.getPublished(16L)).willReturn(episode); + given(signer.sign(anyString())).willAnswer(invocation -> invocation.getArgument(0) + "?s=1"); + MediaRendition ready = new MediaRendition(); + ready.setQuality("720p"); + ready.setPlayUrl("/files/transcoded/16/720p/index.m3u8"); + ready.setStatus("ready"); + MediaRendition failed = new MediaRendition(); + failed.setQuality("1080p"); + failed.setPlayUrl("/files/transcoded/16/1080p/index.m3u8"); + failed.setStatus("failed"); + given(transcodeService.listRenditions(16L)).willReturn(List.of(ready, failed)); + + PlayRespVO resp = playbackService.play(null, 16L, "device-9", "203.0.113.8"); + + assertThat(resp.getTracks()).extracting(track -> track.getQuality()) + .containsExactly("AUTO", "720p"); + assertThat(resp.getTracks().get(1).getPlayUrl()) + .isEqualTo("/files/transcoded/16/720p/index.m3u8?s=1"); + } + private DramaEpisode episode(Long id, int isFree, int unlockCoin, String videoUrl) { DramaEpisode episode = new DramaEpisode(); episode.setId(id); diff --git a/fruitshorts-module-playback/src/main/java/com/fruitshorts/playback/service/PlaybackService.java b/fruitshorts-module-playback/src/main/java/com/fruitshorts/playback/service/PlaybackService.java index 0404c61..8dff5f3 100644 --- a/fruitshorts-module-playback/src/main/java/com/fruitshorts/playback/service/PlaybackService.java +++ b/fruitshorts-module-playback/src/main/java/com/fruitshorts/playback/service/PlaybackService.java @@ -13,6 +13,7 @@ import com.fruitshorts.media.service.TranscodeService; import com.fruitshorts.playback.dal.entity.PlaybackTicket; import com.fruitshorts.playback.dal.mapper.PlaybackTicketMapper; import com.fruitshorts.playback.vo.PlayRespVO; +import com.fruitshorts.playback.vo.PlayTrackVO; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -87,6 +88,7 @@ public class PlaybackService { vo.setAccessible(true); vo.setAccessType(entitled && !free ? ACCESS_ENTITLED : ACCESS_FREE); vo.setPlayUrl(signedUrl); + vo.setTracks(tracksOf(episode.getId(), signedUrl)); if (userId != null) { recordTicket(userId, drama.getId(), episode.getId(), signedUrl, deviceId, clientIp); @@ -115,6 +117,29 @@ public class PlaybackService { return qualities; } + /** AUTO 用主列表,其余档位用各自的 index.m3u8,地址都签名。 */ + private List tracksOf(Long episodeId, String signedAuto) { + List tracks = new ArrayList<>(); + tracks.add(track("AUTO", signedAuto)); + for (MediaRendition rendition : transcodeService.listRenditions(episodeId)) { + if (!StringUtils.hasText(rendition.getPlayUrl())) { + continue; + } + if (!"ready".equalsIgnoreCase(rendition.getStatus())) { + continue; + } + tracks.add(track(rendition.getQuality(), signer.sign(rendition.getPlayUrl()))); + } + return tracks; + } + + private PlayTrackVO track(String quality, String playUrl) { + PlayTrackVO track = new PlayTrackVO(); + track.setQuality(quality); + track.setPlayUrl(playUrl); + return track; + } + private void recordTicket(Long userId, Long dramaId, Long episodeId, String playUrl, String deviceId, String clientIp) { PlaybackTicket ticket = new PlaybackTicket(); diff --git a/fruitshorts-module-playback/src/main/java/com/fruitshorts/playback/vo/PlayRespVO.java b/fruitshorts-module-playback/src/main/java/com/fruitshorts/playback/vo/PlayRespVO.java index c2c2682..3ddecfe 100644 --- a/fruitshorts-module-playback/src/main/java/com/fruitshorts/playback/vo/PlayRespVO.java +++ b/fruitshorts-module-playback/src/main/java/com/fruitshorts/playback/vo/PlayRespVO.java @@ -23,6 +23,8 @@ public class PlayRespVO { private String playUrl; /** 可用的清晰度档位 */ private List qualities; + /** 各档签名地址。AUTO 是主播放列表,其余是单档 HLS。 */ + private List tracks; /** 雪碧图 WebVTT 时间轴地址;没有产物时为 null */ private String thumbnailsUrl; /** 需要多少金币解锁 */ diff --git a/fruitshorts-module-playback/src/main/java/com/fruitshorts/playback/vo/PlayTrackVO.java b/fruitshorts-module-playback/src/main/java/com/fruitshorts/playback/vo/PlayTrackVO.java new file mode 100644 index 0000000..7c8f3db --- /dev/null +++ b/fruitshorts-module-playback/src/main/java/com/fruitshorts/playback/vo/PlayTrackVO.java @@ -0,0 +1,16 @@ +// Author: fruit.com.cn + +package com.fruitshorts.playback.vo; + +import lombok.Data; + +/** 一档可切换的清晰度及其签名播放地址。 */ +@Data +public class PlayTrackVO { + + /** AUTO 或 360p/480p/720p/1080p。 */ + private String quality; + + /** 签名后的播放地址,客户端原样使用。 */ + private String playUrl; +}