test(media): 覆盖转码产物查询接口
This commit is contained in:
1 parent
f66eae5baf
commit
090f149b31
10 files changed
+625
No files matched your search
@@ -25,5 +25,10 @@
|
||||
<groupId>com.fruitshorts</groupId>
|
||||
<artifactId>fruitshorts-module-system</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
+213
@@ -0,0 +1,213 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
package com.fruitshorts.media.service;
|
||||
|
||||
import com.fruitshorts.common.exception.ServiceException;
|
||||
import com.fruitshorts.media.dal.entity.MediaAsset;
|
||||
import com.fruitshorts.media.dal.entity.MediaRendition;
|
||||
import com.fruitshorts.media.dal.entity.MediaThumbnail;
|
||||
import com.fruitshorts.media.dal.entity.TranscodeTask;
|
||||
import com.fruitshorts.media.dal.mapper.MediaAssetMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
class MediaTranscodeOutputServiceTest {
|
||||
|
||||
private static final Long MEDIA_ASSET_ID = 88L;
|
||||
private static final Long EPISODE_ID = 31L;
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
private MediaAssetMapper mediaAssetMapper;
|
||||
private TranscodeService transcodeService;
|
||||
private MediaStorageService storageService;
|
||||
private PlaybackSigner signer;
|
||||
private MediaTranscodeOutputService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
mediaAssetMapper = mock(MediaAssetMapper.class);
|
||||
transcodeService = mock(TranscodeService.class);
|
||||
storageService = mock(MediaStorageService.class);
|
||||
signer = mock(PlaybackSigner.class);
|
||||
service = new MediaTranscodeOutputService(
|
||||
mediaAssetMapper, transcodeService, storageService, signer);
|
||||
given(signer.sign(anyString())).willAnswer(invocation -> invocation.getArgument(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_return_renditions_sorted_from_highest_to_lowest() {
|
||||
givenMedia();
|
||||
given(transcodeService.listRenditionsByMediaAssetId(MEDIA_ASSET_ID))
|
||||
.willReturn(List.of(
|
||||
rendition(1L, "360p", 640, 360, 400),
|
||||
rendition(2L, "1080p", 1920, 1080, 5000),
|
||||
rendition(3L, "720p", 1280, 720, 2500)));
|
||||
|
||||
var result = service.renditions(MEDIA_ASSET_ID);
|
||||
|
||||
assertThat(result).extracting(vo -> vo.getQuality())
|
||||
.containsExactly("1080p", "720p", "360p");
|
||||
assertThat(result.get(0).getQualityName()).isEqualTo("1080P");
|
||||
assertThat(result.get(0).getResolution()).isEqualTo("1920x1080");
|
||||
assertThat(result.get(0).getVideoBitrateKbps()).isEqualTo(5000);
|
||||
assertThat(result.get(0).getVideoCodec()).isEqualTo("H.264");
|
||||
assertThat(result.get(0).getAudioCodec()).isEqualTo("AAC");
|
||||
assertThat(result.get(0).getContainer()).isEqualTo("HLS");
|
||||
assertThat(result.get(0).getHlsUrl()).isEqualTo("/files/master.m3u8");
|
||||
assertThat(result.get(0).getSizeBytes()).isEqualTo(4096L);
|
||||
assertThat(result.get(0).getStatus()).isEqualTo("ready");
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_return_not_found_when_media_does_not_exist() {
|
||||
given(mediaAssetMapper.selectById(404L)).willReturn(null);
|
||||
|
||||
assertThatThrownBy(() -> service.renditions(404L))
|
||||
.isInstanceOf(ServiceException.class)
|
||||
.hasMessage("媒资不存在");
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_return_empty_thumbnail_status_when_product_is_missing() {
|
||||
givenMedia();
|
||||
given(transcodeService.findLatestTaskByMediaAssetId(MEDIA_ASSET_ID))
|
||||
.willReturn(task(EPISODE_ID));
|
||||
given(transcodeService.findThumbnailByEpisodeId(EPISODE_ID)).willReturn(null);
|
||||
|
||||
var result = service.thumbnails(MEDIA_ASSET_ID, 1, 100);
|
||||
|
||||
assertThat(result.getStatus()).isEqualTo("EMPTY");
|
||||
assertThat(result.getFrames().getTotal()).isZero();
|
||||
assertThat(result.getFrames().getList()).isEmpty();
|
||||
assertThat(result.getSprite()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_page_vtt_frames_and_expose_sprite_metadata() throws Exception {
|
||||
givenMedia();
|
||||
given(transcodeService.findLatestTaskByMediaAssetId(MEDIA_ASSET_ID))
|
||||
.willReturn(task(EPISODE_ID));
|
||||
MediaThumbnail thumbnail = thumbnail();
|
||||
given(transcodeService.findThumbnailByEpisodeId(EPISODE_ID)).willReturn(thumbnail);
|
||||
writeVtt("""
|
||||
WEBVTT
|
||||
|
||||
00:00:00.000 --> 00:00:01.000
|
||||
sprite_001.jpg#xywh=0,0,160,90
|
||||
|
||||
00:00:01.000 --> 00:00:02.000
|
||||
sprite_001.jpg#xywh=160,0,160,90
|
||||
|
||||
00:00:02.000 --> 00:00:03.000
|
||||
sprite_001.jpg#xywh=320,0,160,90
|
||||
|
||||
00:00:03.000 --> 00:00:04.000
|
||||
sprite_002.jpg#xywh=0,0,160,90
|
||||
""");
|
||||
given(storageService.fileFor("transcoded/8/thumbnails/thumbnails.vtt"))
|
||||
.willReturn(tempDir.resolve("thumbnails.vtt"));
|
||||
given(storageService.toPublicUrl("transcoded/8/thumbnails/thumbnails.vtt"))
|
||||
.willReturn("/files/transcoded/8/thumbnails/thumbnails.vtt");
|
||||
given(storageService.toPublicUrl("transcoded/8/thumbnails/sprite_001.jpg"))
|
||||
.willReturn("/files/transcoded/8/thumbnails/sprite_001.jpg");
|
||||
given(storageService.toPublicUrl("transcoded/8/thumbnails/sprite_002.jpg"))
|
||||
.willReturn("/files/transcoded/8/thumbnails/sprite_002.jpg");
|
||||
|
||||
var result = service.thumbnails(MEDIA_ASSET_ID, 2, 2);
|
||||
|
||||
assertThat(result.getStatus()).isEqualTo("READY");
|
||||
assertThat(result.getPageIndex()).isEqualTo(2);
|
||||
assertThat(result.getPageSize()).isEqualTo(2);
|
||||
assertThat(result.getFrames().getTotal()).isEqualTo(4);
|
||||
assertThat(result.getFrames().getList()).hasSize(2);
|
||||
assertThat(result.getFrames().getList().get(0).getFrameIndex()).isEqualTo(2);
|
||||
assertThat(result.getFrames().getList().get(0).getTimeMs()).isEqualTo(2000);
|
||||
assertThat(result.getFrames().getList().get(0).getImageUrl())
|
||||
.isEqualTo("/files/transcoded/8/thumbnails/sprite_001.jpg#xywh=320,0,160,90");
|
||||
assertThat(result.getSprite().getColumns()).isEqualTo(2);
|
||||
assertThat(result.getSprite().getRows()).isEqualTo(2);
|
||||
assertThat(result.getSprite().getTotalFrames()).isEqualTo(4);
|
||||
assertThat(result.getSprite().getThumbWidth()).isEqualTo(160);
|
||||
assertThat(result.getSprite().getThumbHeight()).isEqualTo(90);
|
||||
assertThat(result.getSprite().getSpriteUrls()).hasSize(2);
|
||||
assertThat(result.getSprite().getVttUrl())
|
||||
.isEqualTo("/files/transcoded/8/thumbnails/thumbnails.vtt");
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_clamp_thumbnail_page_size_to_five_hundred() {
|
||||
givenMedia();
|
||||
given(transcodeService.findLatestTaskByMediaAssetId(MEDIA_ASSET_ID))
|
||||
.willReturn(task(EPISODE_ID));
|
||||
given(transcodeService.findThumbnailByEpisodeId(EPISODE_ID))
|
||||
.willReturn(thumbnail());
|
||||
|
||||
var result = service.thumbnails(MEDIA_ASSET_ID, 1, 501);
|
||||
|
||||
assertThat(result.getPageIndex()).isEqualTo(1);
|
||||
assertThat(result.getPageSize()).isEqualTo(500);
|
||||
}
|
||||
|
||||
private void givenMedia() {
|
||||
MediaAsset asset = new MediaAsset();
|
||||
asset.setId(MEDIA_ASSET_ID);
|
||||
asset.setFileName("video.mp4");
|
||||
given(mediaAssetMapper.selectById(MEDIA_ASSET_ID)).willReturn(asset);
|
||||
}
|
||||
|
||||
private MediaRendition rendition(Long id, String quality, int width, int height, int bitrate) {
|
||||
MediaRendition rendition = new MediaRendition();
|
||||
rendition.setId(id);
|
||||
rendition.setQuality(quality);
|
||||
rendition.setWidth(width);
|
||||
rendition.setHeight(height);
|
||||
rendition.setVideoBitrate(bitrate);
|
||||
rendition.setAudioBitrate(64);
|
||||
rendition.setFormat("HLS");
|
||||
rendition.setPlayUrl("/files/master.m3u8");
|
||||
rendition.setSizeBytes(4096L);
|
||||
rendition.setStatus("ready");
|
||||
return rendition;
|
||||
}
|
||||
|
||||
private TranscodeTask task(Long episodeId) {
|
||||
TranscodeTask task = new TranscodeTask();
|
||||
task.setId(8L);
|
||||
task.setMediaAssetId(MEDIA_ASSET_ID);
|
||||
task.setEpisodeId(episodeId);
|
||||
task.setStatus(TranscodeService.STATUS_SUCCESS);
|
||||
return task;
|
||||
}
|
||||
|
||||
private MediaThumbnail thumbnail() {
|
||||
MediaThumbnail thumbnail = new MediaThumbnail();
|
||||
thumbnail.setEpisodeId(EPISODE_ID);
|
||||
thumbnail.setVttObjectKey("transcoded/8/thumbnails/thumbnails.vtt");
|
||||
thumbnail.setSpritePrefix("transcoded/8/thumbnails/sprite_");
|
||||
thumbnail.setColumns(2);
|
||||
thumbnail.setRows(2);
|
||||
thumbnail.setThumbWidth(160);
|
||||
thumbnail.setThumbHeight(90);
|
||||
thumbnail.setFrameCount(4);
|
||||
thumbnail.setIntervalSec(1);
|
||||
thumbnail.setStatus(MediaThumbnail.STATUS_READY);
|
||||
return thumbnail;
|
||||
}
|
||||
|
||||
private void writeVtt(String content) throws Exception {
|
||||
Files.writeString(tempDir.resolve("thumbnails.vtt"), content);
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
package com.fruitshorts.media.service;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class ThumbnailVttParserTest {
|
||||
|
||||
@Test
|
||||
void should_parse_timestamps_and_sprite_rectangles() {
|
||||
String vtt = """
|
||||
WEBVTT
|
||||
|
||||
00:00:00.000 --> 00:00:01.000
|
||||
sprite_001.jpg#xywh=0,0,160,90
|
||||
|
||||
00:00:01.500 --> 00:00:02.500
|
||||
sprite_002.jpg#xywh=160,90,160,90
|
||||
""";
|
||||
|
||||
List<ThumbnailVttParser.Cue> cues = ThumbnailVttParser.parse(vtt);
|
||||
|
||||
assertThat(cues).hasSize(2);
|
||||
assertThat(cues.get(0).startMs()).isZero();
|
||||
assertThat(cues.get(0).endMs()).isEqualTo(1000);
|
||||
assertThat(cues.get(0).spriteFile()).isEqualTo("sprite_001.jpg");
|
||||
assertThat(cues.get(1).startMs()).isEqualTo(1500);
|
||||
assertThat(cues.get(1).spriteFile()).isEqualTo("sprite_002.jpg");
|
||||
assertThat(cues.get(1).x()).isEqualTo(160);
|
||||
assertThat(cues.get(1).y()).isEqualTo(90);
|
||||
assertThat(cues.get(1).width()).isEqualTo(160);
|
||||
assertThat(cues.get(1).height()).isEqualTo(90);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_return_empty_list_when_vtt_is_invalid() {
|
||||
assertThat(ThumbnailVttParser.parse("not a vtt")).isEmpty();
|
||||
assertThat(ThumbnailVttParser.parse(null)).isEmpty();
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
package com.fruitshorts.media.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import com.fruitshorts.media.dal.entity.MediaRendition;
|
||||
import com.fruitshorts.media.dal.entity.MediaThumbnail;
|
||||
import com.fruitshorts.media.dal.entity.TranscodeTask;
|
||||
import com.fruitshorts.media.dal.mapper.MediaRenditionMapper;
|
||||
import com.fruitshorts.media.dal.mapper.MediaThumbnailMapper;
|
||||
import com.fruitshorts.media.dal.mapper.TranscodeTaskMapper;
|
||||
import org.apache.ibatis.builder.MapperBuilderAssistant;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
class TranscodeQueueStatsTest {
|
||||
|
||||
@BeforeAll
|
||||
static void initTableInfoCache() {
|
||||
MapperBuilderAssistant assistant =
|
||||
new MapperBuilderAssistant(new MybatisConfiguration(), "");
|
||||
TableInfoHelper.initTableInfo(assistant, TranscodeTask.class);
|
||||
TableInfoHelper.initTableInfo(assistant, MediaRendition.class);
|
||||
TableInfoHelper.initTableInfo(assistant, MediaThumbnail.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_return_typed_total_and_status_counts() {
|
||||
TranscodeTaskMapper taskMapper = mock(TranscodeTaskMapper.class);
|
||||
MediaRenditionMapper renditionMapper = mock(MediaRenditionMapper.class);
|
||||
given(taskMapper.selectCount(any())).willReturn(10L, 2L, 1L, 5L, 1L, 1L);
|
||||
given(renditionMapper.selectCount(null)).willReturn(20L);
|
||||
TranscodeService service = new TranscodeService(
|
||||
taskMapper, renditionMapper, mock(MediaThumbnailMapper.class),
|
||||
mock(ThumbnailRecordService.class));
|
||||
|
||||
var stats = service.stats();
|
||||
|
||||
assertThat(stats.getTotal()).isEqualTo(10L);
|
||||
assertThat(stats.getQueued()).isEqualTo(2L);
|
||||
assertThat(stats.getRunning()).isEqualTo(1L);
|
||||
assertThat(stats.getSuccess()).isEqualTo(5L);
|
||||
assertThat(stats.getFailed()).isEqualTo(1L);
|
||||
assertThat(stats.getCanceled()).isEqualTo(1L);
|
||||
assertThat(stats.getRenditions()).isEqualTo(20L);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user