Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ jobs:
with:
dotnet-version: '10.0.x'

- name: Install FFmpeg
run: |
sudo apt-get update
sudo apt-get install -y ffmpeg

- name: Run tests
run: >-
dotnet test SecondDimensionWatcherReDive.slnx -c Release
Expand Down
3 changes: 3 additions & 0 deletions Containerfile
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ RUN dotnet publish SecondDimensionWatcherReDive/SecondDimensionWatcherReDive.csp
# Stage 3: Runtime
FROM mcr.microsoft.com/dotnet/aspnet:10.0
WORKDIR /app
RUN apt-get update \
&& apt-get install -y --no-install-recommends ffmpeg \
&& rm -rf /var/lib/apt/lists/*
COPY --from=backend-build /app .
EXPOSE 8080
# Optional: read-only NFSv4 export (set Nfs:Enabled=true to activate; publish port at run time).
Expand Down

This file was deleted.

98 changes: 97 additions & 1 deletion SecondDimensionWatcherReDive.Client/mock-server.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1369,7 +1369,14 @@ async function route(method, pathname, searchParams, req, res) {
}

// --- All remaining endpoints require auth ---
if (!hasAuth(req) && !pathname.startsWith("/api/auth/")) {
const publicTranscodingSession =
(method === "GET" || method === "DELETE") &&
pathname.startsWith("/api/transcoding/sessions/");
if (
!hasAuth(req) &&
!pathname.startsWith("/api/auth/") &&
!publicTranscodingSession
) {
return empty(res, 401);
}

Expand Down Expand Up @@ -2498,6 +2505,95 @@ async function route(method, pathname, searchParams, req, res) {

// --- Files ---

if (method === "POST" && pathname === "/api/transcoding/prepare") {
const sessionId = randomUUID();
const token = randomBytes(32).toString("hex");
const base = `/api/transcoding/sessions/${sessionId}`;
return json(res, {
sessionId,
state: "ready",
strategy: "remux",
isPlayable: true,
cacheHit: false,
progress: 1,
speed: 8.5,
queuePosition: null,
error: null,
videoCodec: "h264",
audioCodec: "aac",
statusUrl: `${base}?token=${token}`,
cancelUrl: `${base}?token=${token}`,
playbackUrl: `${base}/media.m3u8?token=${token}`,
subtitles: [],
unsupportedSubtitleCount: 0,
});
}

const transcodeSessionMatch = pathname.match(
/^\/api\/transcoding\/sessions\/([^/]+)\/([^/]+)(?:\/([^/]+))?$/,
);
if (method === "GET" && transcodeSessionMatch?.[2] === "media.m3u8") {
const sessionId = transcodeSessionMatch[1];
const token = searchParams.get("token") ?? "";
res.writeHead(200, {
"Content-Type": "application/vnd.apple.mpegurl",
"Cache-Control": "no-cache, no-store",
});
return res.end(
`#EXTM3U\n#EXT-X-VERSION:3\n#EXTINF:6,\n/api/transcoding/sessions/${sessionId}/segments/segment-000000.ts?token=${token}\n#EXT-X-ENDLIST\n`,
);
}
if (
method === "GET" &&
transcodeSessionMatch?.[2] === "segments" &&
transcodeSessionMatch?.[3]
) {
res.writeHead(200, { "Content-Type": "video/mp2t" });
return res.end("Mock HLS segment");
}
const transcodeStatusMatch = pathname.match(
/^\/api\/transcoding\/sessions\/([^/]+)$/,
);
if (method === "GET" && transcodeStatusMatch) {
const sessionId = transcodeStatusMatch[1];
const token = searchParams.get("token") ?? "";
const base = `/api/transcoding/sessions/${sessionId}`;
return json(res, {
sessionId,
state: "ready",
strategy: "remux",
isPlayable: true,
cacheHit: true,
progress: 1,
speed: 8.5,
queuePosition: null,
error: null,
videoCodec: "h264",
audioCodec: "aac",
statusUrl: `${base}?token=${token}`,
cancelUrl: `${base}?token=${token}`,
playbackUrl: `${base}/media.m3u8?token=${token}`,
subtitles: [],
unsupportedSubtitleCount: 0,
});
}
if (method === "DELETE" && transcodeStatusMatch) return empty(res, 204);

if (method === "GET" && pathname === "/api/transcoding/metrics") {
return json(res, {
queuedJobs: 0,
activeJobs: 0,
completedJobs: 1,
failedJobs: 0,
canceledJobs: 0,
cacheHits: 1,
cacheBytes: 1048576,
averageFirstSegmentSeconds: 0.8,
averageTranscodeSpeed: 8.5,
failureRate: 0,
});
}

if (method === "GET" && pathname === "/api/file/list") {
const id = searchParams.get("id");
const relativeDir = searchParams.get("relativeDir") ?? "";
Expand Down
3 changes: 1 addition & 2 deletions SecondDimensionWatcherReDive.Client/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
{
"dependencies": {
"@ffmpeg/core": "^0.12.10",
"@ffmpeg/ffmpeg": "patch:@ffmpeg/ffmpeg@npm%3A0.12.15#~/.yarn/patches/@ffmpeg-ffmpeg-npm-0.12.15-ef17f09f1b.patch",
"@radix-ui/react-dialog": "^1.1.23",
"@radix-ui/react-dropdown-menu": "^2.1.24",
"@radix-ui/react-progress": "^1.1.16",
Expand All @@ -12,6 +10,7 @@
"artplayer-proxy-mediabunny": "^1.2.0",
"clsx": "^2.1.1",
"dayjs": "^1.11.23",
"hls.js": "^1.7.1",
"i18next": "^26.4.0",
"i18next-browser-languagedetector": "^8.2.1",
"lucide-react": "^1.34.0",
Expand Down
24 changes: 13 additions & 11 deletions SecondDimensionWatcherReDive.Client/src/i18n/locales/en/player.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,25 +56,27 @@
"mkv": {
"mode": {
"demuxed": "Browser MKV demux",
"transcoded": "Browser transcoded"
"serverRemuxed": "Server HLS remux",
"serverTranscoded": "Server HLS transcode"
},
"stages": {
"probing": "Inspecting MKV audio and video tracks",
"extractingSubtitles": "Extracting and converting embedded subtitles",
"loadingTranscoder": "Loading the browser transcoder",
"downloading": "Downloading the MKV for browser conversion",
"readingTracks": "Reading MKV track information",
"convertingSubtitles": "Converting embedded subtitles to WebVTT",
"transcodingVideo": "Converting unsupported audio or video codecs",
"finalizing": "Preparing the playable video"
"serverQueued": "Waiting in the server transcoding queue (position {{position}})",
"serverProbing": "The server is inspecting media tracks",
"serverRemuxing": "Remuxing compatible tracks into HLS",
"serverTranscoding": "Transcoding unsupported tracks into HLS",
"serverFinalizing": "Streaming generated HLS segments while the server finishes"
},
"playbackPreparationFailed": "This MKV could not be demuxed or converted in the browser",
"playbackPreparationFailed": "The server could not prepare this MKV for browser playback",
"subtitleExtractionFailed": "The MKV can play, but its embedded subtitles could not be extracted",
"transcodeNotice": "Browser software conversion downloads the complete file and can take a while. Keep this page open.",
"serverNotice": "Playback starts as soon as the first server segment is ready; the source file is not downloaded to this device.",
"subtitleNotice": "The video is ready while compatible embedded subtitles are prepared in the background.",
"cacheHit": "Cached segments",
"codecSummary": "MKV demuxed automatically · video {{video}} · audio {{audio}}",
"noAudio": "none",
"bitmapSubtitlesSkipped_one": "Skipped {{count}} subtitle track that the browser could not convert",
"bitmapSubtitlesSkipped_other": "Skipped {{count}} subtitle tracks that the browser could not convert"
"bitmapSubtitlesSkipped_one": "{{count}} bitmap subtitle track is unavailable unless server burn-in is enabled",
"bitmapSubtitlesSkipped_other": "{{count}} bitmap subtitle tracks are unavailable unless server burn-in is enabled"
},
"next": {
"play": "Next episode",
Expand Down
22 changes: 12 additions & 10 deletions SecondDimensionWatcherReDive.Client/src/i18n/locales/ja/player.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,24 +53,26 @@
"mkv": {
"mode": {
"demuxed": "ブラウザーで MKV 分離",
"transcoded": "ブラウザーで変換済み"
"serverRemuxed": "サーバー HLS リマックス",
"serverTranscoded": "サーバー HLS 変換"
},
"stages": {
"probing": "MKV の映像・音声トラックを確認しています",
"extractingSubtitles": "内蔵字幕を抽出して変換しています",
"loadingTranscoder": "ブラウザー変換エンジンを読み込んでいます",
"downloading": "ブラウザー変換用に MKV をダウンロードしています",
"readingTracks": "MKV のトラック情報を読み取っています",
"convertingSubtitles": "内蔵字幕を WebVTT に変換しています",
"transcodingVideo": "未対応の映像・音声コーデックを変換しています",
"finalizing": "再生可能な動画を準備しています"
"serverQueued": "サーバー変換待ちです(キュー {{position}} 番)",
"serverProbing": "サーバーがメディアトラックを確認しています",
"serverRemuxing": "互換トラックを HLS にリマックスしています",
"serverTranscoding": "未対応トラックを HLS に変換しています",
"serverFinalizing": "生成済み HLS を再生しながら残りを処理しています"
},
"playbackPreparationFailed": "この MKV をブラウザーで分離または変換できませんでした",
"playbackPreparationFailed": "サーバーでこの MKV をブラウザー再生用に準備できませんでした",
"subtitleExtractionFailed": "MKV は再生できますが、内蔵字幕を抽出できませんでした",
"transcodeNotice": "ブラウザーでのソフトウェア変換はファイル全体をダウンロードするため、時間がかかる場合があります。このページを開いたままにしてください。",
"serverNotice": "最初のサーバー分割ができ次第再生し、元ファイル全体は端末へダウンロードしません。",
"subtitleNotice": "動画は再生可能です。対応する内蔵字幕をバックグラウンドで準備しています。",
"cacheHit": "キャッシュ済み分割を再利用",
"codecSummary": "MKV を自動分離しました · 映像 {{video}} · 音声 {{audio}}",
"noAudio": "なし",
"bitmapSubtitlesSkipped": "ブラウザーで変換できない字幕トラック {{count}} 件をスキップしました"
"bitmapSubtitlesSkipped": "画像字幕 {{count}} 件は利用できません。サーバー側の焼き込み設定で有効化できます"
},
"next": {
"play": "次のエピソード",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,24 +53,26 @@
"mkv": {
"mode": {
"demuxed": "MKV 前端拆包",
"transcoded": "浏览器已转码"
"serverRemuxed": "服务端 HLS 无损封装",
"serverTranscoded": "服务端 HLS 转码"
},
"stages": {
"probing": "正在检查 MKV 音视频轨道",
"extractingSubtitles": "正在拆出并转换内封字幕",
"loadingTranscoder": "正在加载浏览器转码器",
"downloading": "正在下载 MKV 以供前端转换",
"readingTracks": "正在读取 MKV 轨道信息",
"convertingSubtitles": "正在把内封字幕转换为 WebVTT",
"transcodingVideo": "正在转换浏览器不支持的音视频编码",
"finalizing": "正在生成可播放的视频"
"serverQueued": "正在等待服务端转码(队列第 {{position}} 位)",
"serverProbing": "服务端正在探测媒体轨道",
"serverRemuxing": "正在将兼容轨道无损封装为 HLS",
"serverTranscoding": "正在将不兼容轨道转为 HLS",
"serverFinalizing": "已开始播放生成的分片,服务端继续处理剩余内容"
},
"playbackPreparationFailed": "无法在浏览器中拆包或转换此 MKV",
"playbackPreparationFailed": "服务端无法为浏览器准备此 MKV",
"subtitleExtractionFailed": "MKV 可以播放,但内封字幕拆出失败",
"transcodeNotice": "前端软件转码需要完整下载文件,首次处理可能较慢,请保持此页面打开。",
"serverNotice": "首个服务端分片就绪后即可播放,源文件不会完整下载到本设备。",
"subtitleNotice": "视频已可播放,兼容的内封字幕仍在后台准备。",
"cacheHit": "已复用缓存分片",
"codecSummary": "MKV 已自动拆包 · 视频 {{video}} · 音频 {{audio}}",
"noAudio": "无",
"bitmapSubtitlesSkipped": "已跳过 {{count}} 条浏览器无法转换的字幕轨道"
"bitmapSubtitlesSkipped": "{{count}} 条位图字幕不可用;管理员可配置服务端烧录"
},
"next": {
"play": "下一集",
Expand Down
Loading