fix(media): 补齐分集转码绑定与产物回填
This commit is contained in:
1 parent
6bdd245a83
commit
7d563f2cd6
18 files changed
+812
-25
No files matched your search
+64
@@ -0,0 +1,64 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
package com.fruitshorts.worker;
|
||||
|
||||
import com.fruitshorts.media.dal.entity.MediaAsset;
|
||||
import com.fruitshorts.media.dal.mapper.MediaAssetMapper;
|
||||
import com.fruitshorts.media.service.EpisodeTranscodeBindingService;
|
||||
import com.fruitshorts.media.service.MediaStorageService;
|
||||
import com.fruitshorts.media.service.TranscodeService;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
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;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* 视频 URL -> media_asset -> transcode_task 的分集绑定。
|
||||
*/
|
||||
class EpisodeTranscodeBindingServiceTest {
|
||||
|
||||
private MediaAssetMapper mediaAssetMapper;
|
||||
private MediaStorageService storageService;
|
||||
private TranscodeService transcodeService;
|
||||
private EpisodeTranscodeBindingService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
mediaAssetMapper = mock(MediaAssetMapper.class);
|
||||
storageService = mock(MediaStorageService.class);
|
||||
transcodeService = mock(TranscodeService.class);
|
||||
service = new EpisodeTranscodeBindingService(
|
||||
mediaAssetMapper, storageService, transcodeService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_find_asset_by_object_key_and_bind_task_when_episode_saved() {
|
||||
MediaAsset asset = new MediaAsset();
|
||||
asset.setId(88L);
|
||||
asset.setSourceUrl("/files/episode/202610/video.mp4");
|
||||
given(storageService.objectKeyOf("/files/episode/202610/video.mp4?e=1&s=x"))
|
||||
.willReturn("episode/202610/video.mp4");
|
||||
given(mediaAssetMapper.selectOne(any())).willReturn(asset);
|
||||
|
||||
Long assetId = service.bind(
|
||||
31L, 7L, "/files/episode/202610/video.mp4?e=1&s=x");
|
||||
|
||||
assertThat(assetId).isEqualTo(88L);
|
||||
verify(transcodeService).bindToEpisode(
|
||||
88L, 7L, 31L, "/files/episode/202610/video.mp4");
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_return_null_when_video_url_has_no_uploaded_asset() {
|
||||
given(storageService.objectKeyOf("https://external.test/video.mp4"))
|
||||
.willReturn("https://external.test/video.mp4");
|
||||
given(mediaAssetMapper.selectOne(any())).willReturn(null);
|
||||
|
||||
assertThat(service.bind(31L, 7L, "https://external.test/video.mp4"))
|
||||
.isNull();
|
||||
}
|
||||
}
|
||||
+4
-1
@@ -62,6 +62,8 @@ class FfmpegTranscodeRunnerSmokeTest {
|
||||
assertThat(result.durationSec()).isBetween(1, 5);
|
||||
assertThat(result.thumbnail()).isNotNull();
|
||||
assertThat(result.thumbnail().status()).isEqualTo("READY");
|
||||
assertThat(result.thumbnail().thumbWidth()).isEqualTo(160);
|
||||
assertThat(result.thumbnail().thumbHeight()).isEqualTo(90);
|
||||
assertThat(Files.exists(output.resolve("thumbnails/thumbnails.vtt"))).isTrue();
|
||||
assertThat(Files.exists(output.resolve("thumbnails/sprite_001.jpg"))).isTrue();
|
||||
assertThat(readLines(output.resolve("master.m3u8")))
|
||||
@@ -76,7 +78,8 @@ class FfmpegTranscodeRunnerSmokeTest {
|
||||
Process process = new ProcessBuilder(ffmpeg, "-y",
|
||||
"-f", "lavfi", "-i", "testsrc=size=640x360:rate=15",
|
||||
"-f", "lavfi", "-i", "sine=frequency=440:duration=2",
|
||||
"-shortest", "-c:v", "libx264", "-pix_fmt", "yuv420p", "-c:a", "aac",
|
||||
"-shortest", "-t", "2",
|
||||
"-c:v", "libx264", "-pix_fmt", "yuv420p", "-c:a", "aac",
|
||||
sample.toString())
|
||||
.redirectErrorStream(true)
|
||||
.start();
|
||||
|
||||
+62
-1
@@ -3,6 +3,7 @@
|
||||
package com.fruitshorts.worker;
|
||||
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import com.fruitshorts.common.exception.ServiceException;
|
||||
import com.fruitshorts.media.config.MediaProperties;
|
||||
@@ -14,10 +15,12 @@ import com.fruitshorts.media.dal.mapper.MediaThumbnailMapper;
|
||||
import com.fruitshorts.media.dal.mapper.TranscodeTaskMapper;
|
||||
import com.fruitshorts.media.service.MediaStorageService;
|
||||
import com.fruitshorts.media.service.TranscodeService;
|
||||
import com.fruitshorts.media.service.ThumbnailRecordService;
|
||||
import org.apache.ibatis.builder.MapperBuilderAssistant;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
@@ -27,6 +30,7 @@ import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
@@ -61,8 +65,11 @@ class TranscodeClaimAndStorageTest {
|
||||
transcodeTaskMapper = mock(TranscodeTaskMapper.class);
|
||||
mediaRenditionMapper = mock(MediaRenditionMapper.class);
|
||||
mediaThumbnailMapper = mock(MediaThumbnailMapper.class);
|
||||
ThumbnailRecordService thumbnailRecordService =
|
||||
new ThumbnailRecordService(mediaThumbnailMapper);
|
||||
transcodeService = new TranscodeService(
|
||||
transcodeTaskMapper, mediaRenditionMapper, mediaThumbnailMapper);
|
||||
transcodeTaskMapper, mediaRenditionMapper,
|
||||
mediaThumbnailMapper, thumbnailRecordService);
|
||||
mediaProperties = new MediaProperties();
|
||||
mediaProperties.setRootDir("/tmp/fruitshorts-storage-test");
|
||||
mediaProperties.setPublicPrefix("/files");
|
||||
@@ -89,6 +96,59 @@ class TranscodeClaimAndStorageTest {
|
||||
assertThat(claimed.getId()).isEqualTo(2L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_exclude_unbound_queued_task_from_claim_query() {
|
||||
given(transcodeTaskMapper.selectOne(any())).willReturn(null);
|
||||
|
||||
assertThat(transcodeService.claimNext()).isNull();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
ArgumentCaptor<Wrapper<TranscodeTask>> captor = ArgumentCaptor.forClass(Wrapper.class);
|
||||
verify(transcodeTaskMapper).selectOne(captor.capture());
|
||||
assertThat(captor.getValue().getSqlSegment()).contains("episode_id >");
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_reuse_and_backfill_existing_task_when_episode_is_bound() {
|
||||
TranscodeTask existing = task(4L);
|
||||
existing.setMediaAssetId(88L);
|
||||
existing.setEpisodeId(0L);
|
||||
existing.setDramaId(0L);
|
||||
given(transcodeTaskMapper.selectOne(any())).willReturn(existing);
|
||||
|
||||
Long taskId = transcodeService.bindToEpisode(
|
||||
88L, 7L, 31L, "/files/episode/202610/video.mp4");
|
||||
|
||||
assertThat(taskId).isEqualTo(4L);
|
||||
assertThat(existing.getDramaId()).isEqualTo(7L);
|
||||
assertThat(existing.getEpisodeId()).isEqualTo(31L);
|
||||
verify(transcodeTaskMapper).update(any(), any());
|
||||
verify(transcodeTaskMapper, never()).insert(any(TranscodeTask.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_keep_main_success_when_thumbnail_record_transaction_fails() {
|
||||
TranscodeTask task = task(5L);
|
||||
task.setDramaId(7L);
|
||||
task.setEpisodeId(31L);
|
||||
given(transcodeTaskMapper.selectById(5L)).willReturn(task);
|
||||
ThumbnailRecordService failingRecordService = mock(ThumbnailRecordService.class);
|
||||
doThrow(new IllegalStateException("media_thumbnail does not exist"))
|
||||
.when(failingRecordService).upsert(any(), any());
|
||||
TranscodeService guardedService = new TranscodeService(
|
||||
transcodeTaskMapper, mediaRenditionMapper,
|
||||
mediaThumbnailMapper, failingRecordService);
|
||||
MediaThumbnail thumbnail = new MediaThumbnail();
|
||||
thumbnail.setEpisodeId(31L);
|
||||
thumbnail.setStatus(MediaThumbnail.STATUS_READY);
|
||||
|
||||
guardedService.succeed(5L, java.util.List.of(), thumbnail);
|
||||
|
||||
assertThat(task.getStatus()).isEqualTo(TranscodeService.STATUS_SUCCESS);
|
||||
assertThat(task.getErrorMsg()).contains("缩略图登记失败");
|
||||
verify(transcodeTaskMapper).updateById(task);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_return_null_when_queue_is_empty() {
|
||||
given(transcodeTaskMapper.selectOne(any())).willReturn(null);
|
||||
@@ -143,6 +203,7 @@ class TranscodeClaimAndStorageTest {
|
||||
task.setId(id);
|
||||
task.setStatus(TranscodeService.STATUS_QUEUED);
|
||||
task.setRetryCount(0);
|
||||
task.setEpisodeId(11L);
|
||||
return task;
|
||||
}
|
||||
}
|
||||
+6
-1
@@ -9,6 +9,7 @@ import com.fruitshorts.media.dal.entity.TranscodeTask;
|
||||
import com.fruitshorts.media.service.MediaService;
|
||||
import com.fruitshorts.media.service.MediaStorageService;
|
||||
import com.fruitshorts.media.service.MediaTranscodeProfileService;
|
||||
import com.fruitshorts.media.service.EpisodeTranscodeCompletionPort;
|
||||
import com.fruitshorts.media.service.TranscodeService;
|
||||
import com.fruitshorts.media.service.transcode.TranscodeRunner;
|
||||
import com.fruitshorts.media.service.transcode.TranscodeWorker;
|
||||
@@ -44,6 +45,7 @@ class TranscodeWorkerTest {
|
||||
private MediaStorageService storageService;
|
||||
private StubRunner runner;
|
||||
private MediaTranscodeProfileService profileService;
|
||||
private EpisodeTranscodeCompletionPort episodeCompletionPort;
|
||||
private TranscodeWorker worker;
|
||||
|
||||
@BeforeEach
|
||||
@@ -52,9 +54,10 @@ class TranscodeWorkerTest {
|
||||
mediaService = mock(MediaService.class);
|
||||
storageService = mock(MediaStorageService.class);
|
||||
profileService = mock(MediaTranscodeProfileService.class);
|
||||
episodeCompletionPort = mock(EpisodeTranscodeCompletionPort.class);
|
||||
runner = new StubRunner();
|
||||
worker = new TranscodeWorker(transcodeService, mediaService, storageService, runner,
|
||||
profileService);
|
||||
profileService, episodeCompletionPort);
|
||||
given(profileService.currentProfile()).willReturn(new MediaTranscodeProfile());
|
||||
given(profileService.ffmpegPath()).willReturn("ffmpeg");
|
||||
given(profileService.ffprobePath()).willReturn("ffprobe");
|
||||
@@ -87,6 +90,8 @@ class TranscodeWorkerTest {
|
||||
.isEqualTo(MediaThumbnail.STATUS_READY);
|
||||
verify(mediaService).markReady(eq(ASSET_ID), eq("/files/transcoded/88/master.m3u8"),
|
||||
eq("/files/transcoded/88/poster.jpg"), eq(120));
|
||||
verify(episodeCompletionPort).markEpisodeReady(
|
||||
12L, "/files/transcoded/88/master.m3u8", 120);
|
||||
verify(transcodeService, never()).fail(any(), anyString());
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user