Skip to content

Commit

Permalink
Implement subtitle upload and delete
Browse files Browse the repository at this point in the history
  • Loading branch information
elonen committed Jun 4, 2024
1 parent c7342cc commit 4629dbc
Show file tree
Hide file tree
Showing 12 changed files with 475 additions and 41 deletions.
102 changes: 83 additions & 19 deletions client/src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ function onDisplayComment(e: any) {
loop: videoPlayer.isLooping(),
seekTimeSec: videoPlayer.getCurTime(),
drawing: e.detail.drawing,
subtitleId: $curSubtitle?.id
}});
}
}
Expand Down Expand Up @@ -182,12 +183,7 @@ function onPlayerSeeked(_e: any) {
function onCollabReport(e: any) {
if ($collabId)
wsEmit({collabReport: {
paused: e.detail.paused,
loop: e.detail.loop,
seekTimeSec: e.detail.seek_time,
drawing: e.detail.drawing,
}});
wsEmit({collabReport: e.report});
}
function onCommentPinClicked(e: any) {
Expand All @@ -205,20 +201,75 @@ function onCommentPinClicked(e: any) {
}
}
// <button class="text-gray-300" on:click={onSubtitleChange} data-subtitle-id={sub.id}>
function onSubtitleChange(sub_id: string) {
console.log("onSubtitleChange: ", sub_id);
function onSubtitleChange(e: any) {
const sub_id = e.detail.id;
console.debug("onSubtitleChange, id:", sub_id);
if ($curSubtitle?.id == sub_id) {
$curSubtitle = null;
} else {
$curSubtitle = $allSubtitles.find((s) => s.id == sub_id) ?? null;
if ($curSubtitle == null) {
if ($curSubtitle == null && sub_id != null) {
console.error("Subtitle not found: ", sub_id);
acts.add({mode: 'error', message: "Subtitle not found. See log.", lifetime: 5});
}
}
if ($collabId) {
wsEmit({collabReport: {
paused: videoPlayer.isPaused(),
loop: videoPlayer.isLooping(),
seekTimeSec: videoPlayer.getCurTime(),
drawing: videoPlayer.getScreenshot(),
subtitleId: $curSubtitle?.id,
}});
}
console.log("Subtitle URL changed to: ", $curSubtitle?.playbackUrl);
}
// User clicked on subtitle upload icon
async function onUploadSubtitles() {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.srt, .vtt, .ssa, .ass';
input.click();
input.onchange = async () => {
if (!input.files?.length) {
console.log('No subtitle file selected. Skipping upload.');
return;
}
for (let file of input.files) {
const reader = new FileReader();
reader.onload = function(event) {
try {
if (!event.target?.result) { throw new Error('No file contents read'); }
const dataUrl = event.target.result as string;
const [_, contentsBase64] = dataUrl.split(',');
wsEmit({ addSubtitle : {
mediaFileId: $mediaFileId!,
fileName: file.name,
contentsBase64
}});
} catch (e) {
console.error('Error adding subtitle file:', e);
acts.add({mode: 'error', message: 'Error adding subtitle file. See log.', lifetime: 5});
}
};
reader.readAsDataURL(file);
}
};
}
async function deleteSubtitle(id: string) {
if (window.confirm("Are you sure you want to delete this subtitle?")) {
wsEmit({ delSubtitle: { id } });
}
}
async function editSubtitle(id: string) {
window.alert("Not implemented yet");
}
function popHistoryState(e: PopStateEvent) {
console.debug("popHistoryState called. e.state=", e.state);
if (e.state) {
Expand Down Expand Up @@ -633,7 +684,8 @@ function connectWebsocketAfterAuthCheck(ws_url: string)
$videoFps = parseFloat(v.duration.fps);
if (isNaN($videoFps)) throw Error("Invalid FPS");
$videoTitle = v.title;
$allSubtitles = v.subtitles ?? [];
$allSubtitles = [...v.subtitles];
console.debug("new $allSubtitles: ", $allSubtitles);
$allComments = [];
if (v.defaultSubtitleId) {
Expand Down Expand Up @@ -855,14 +907,16 @@ function onMediaFileListPopupAction(e: { detail: { action: Proto3.ActionDef, ite
on:seeked={onPlayerSeeked}
on:collabReport={onCollabReport}
on:commentPinClicked={onCommentPinClicked}
on:uploadSubtitles={onUploadSubtitles}
on:onSubtitleChange={onSubtitleChange}
/>
</div>
<div class="flex-none w-full p-2 {debugLayout?'border-2 border-green-500':''}">
<CommentInput bind:this={commentInput} on:button-clicked={onCommentInputButton} />
</div>
</div>

{#if $allComments.length > 0}
{#if $allComments.length > 0 || $curSubtitle}
<div id="comment_list" transition:fade class="flex flex-col h-full w-72 bg-gray-900 py-2 px-2 ml-2">
<div class="flex-grow overflow-y-auto space-y-2">
{#each $allComments as it}
Expand All @@ -878,16 +932,26 @@ function onMediaFileListPopupAction(e: { detail: { action: Proto3.ActionDef, ite
</div>
<div class="flex-none">
<!-- Subtitles -->
<h6 class="text-gray-500 py-2 border-t border-gray-500">Subtitles</h6>
<div class="flex justify-between text-gray-500 items-center py-2 border-t border-gray-500">
<h6>Subtitles</h6>
<button class="fa fa-plus-circle" title="Upload subtitles" on:click={onUploadSubtitles}></button>
</div>
{#each $allSubtitles as sub}
<!-- new version with toggle on/off + "radio"-style behavior but with buttons and fa eye / eye-slash -->
<div class="flex justify-between items-center {sub.id == $curSubtitle?.id ? "text-amber-600" : "text-gray-300"}">
<button on:click={() => onSubtitleChange(sub.id)} title={sub.origFilename}>
<i class="fa {sub.id == $curSubtitle?.id ? "fa-eye" : "fa-eye-slash" }"></i>
<span class="ml-2">
({sub.languageCode.toUpperCase()}) {sub.title}
</span>
<div class="flex flex-nowrap space-x-1 text-sm whitespace-nowrap justify-between items-center text-gray-400 w-full">
<button
class="flex-grow text-left hover:text-white {sub.id == $curSubtitle?.id ? 'text-amber-600' : 'text-gray-400'} overflow-hidden"
on:click={() => onSubtitleChange({detail: {id: sub.id}})}
title={sub.origFilename}
style="text-overflow: ellipsis; white-space: nowrap;"
>
<i class="fa {sub.id == $curSubtitle?.id ? 'fa-eye' : 'fa-eye-slash' }"></i>
<span class="text-ellipsis"><strong>{sub.languageCode.toUpperCase()}</strong> – {sub.title}</span>
</button>
<span class="flex-shrink-0">
<button class="fa fa-pencil-alt hover:text-white" title="Edit subtitle" on:click={() => editSubtitle(sub.id)}></button>
<button class="fa fa-trash-alt hover:text-white" title="Delete subtitle" on:click={() => deleteSubtitle(sub.id)}></button>
</span>
</div>
{/each}
</div>
Expand Down
57 changes: 44 additions & 13 deletions client/src/lib/player_view/VideoPlayer.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,15 @@ function refreshCommentPins(): void {
function send_collab_report(): void {
if ($collabId) {
let drawing = paused ? getScreenshot() : null;
dispatch('collabReport', {paused: videoElem.paused, loop: videoElem.loop, seek_time: videoElem.currentTime, drawing: drawing});
let drawing = paused ? getScreenshot() : undefined;
let report: Proto3.client.ClientToServerCmd_CollabReport = {
paused: videoElem.paused,
loop: videoElem.loop,
seekTimeSec: videoElem.currentTime,
drawing,
subtitleId: $curSubtitle?.id,
};
dispatch('collabReport', { report });
}
}
Expand Down Expand Up @@ -366,16 +373,29 @@ function onFrameEdited(e: Event) {
send_collab_report();
}
let uploadSubtitlesButton: HTMLButtonElement;
function changeSubtitleUploadIcon(upload_icon: boolean) {
if (upload_icon) {
uploadSubtitlesButton.classList.remove('fa-closed-captioning');
uploadSubtitlesButton.classList.add('fa-upload');
} else {
uploadSubtitlesButton.classList.remove('fa-upload');
uploadSubtitlesButton.classList.add('fa-closed-captioning');
}
}
let prev_subtitle: Proto3.Subtitle|null = null;
function toggleSubtitle() {
// Dispatch to parent instead of setting directly, to allow collab sessions to sync
if ($curSubtitle) {
prev_subtitle = $curSubtitle;
$curSubtitle = null;
dispatch('onSubtitleChange', {id: null});
} else {
if (prev_subtitle) {
$curSubtitle = prev_subtitle;
dispatch('onSubtitleChange', {id: prev_subtitle.id});
} else {
$curSubtitle = $allSubtitles[0];
dispatch('onSubtitleChange', {id: $allSubtitles[0]?.id});
}
}
}
Expand Down Expand Up @@ -404,10 +424,12 @@ function toggleSubtitle() {
bind:currentTime={time}
bind:duration
bind:paused>
<track kind="captions">
{#if $curSubtitle}
<track kind="captions" src={$curSubtitle.playbackUrl} srclang={$curSubtitle.languageCode} label={$curSubtitle.title} default />
{/if}
<track kind="captions"
src="{$curSubtitle?.playbackUrl}"
srclang="en"
label="{$curSubtitle?.title}"
default
/>
</video>

<!-- TODO: maybe show actively controlling collaborator's avatar like this?
Expand Down Expand Up @@ -456,15 +478,24 @@ function toggleSubtitle() {
</span>

<!-- Closed captioning -->
{#if $allSubtitles.length > 0}
<span class="flex-0 text-center whitespace-nowrap">
<span class="flex-0 text-center whitespace-nowrap">
{#if $allSubtitles.length > 0}
<button
class={ $curSubtitle ? 'fa-solid fa-closed-captioning text-amber-600' : 'fa-solid fa-closed-captioning text-gray-400' }
title="Toggle closed captioning"
on:click={() => toggleSubtitle()}
/>
</span>
{/if}
{:else}
<button bind:this={uploadSubtitlesButton}
class="fa-solid fa-closed-captioning text-gray-400" title="Upload subtitles"
on:mouseover={() => { changeSubtitleUploadIcon(true); }}
on:focus={() => { changeSubtitleUploadIcon(true); }}
on:mouseout={() => { changeSubtitleUploadIcon(false); }}
on:blur={() => { changeSubtitleUploadIcon(false); }}
on:click={() => { dispatch('uploadSubtitles', {}); }}
/>
{/if}
</span>

<!-- Audio volume -->
<span class="flex-0 text-center whitespace-nowrap">
Expand Down
28 changes: 28 additions & 0 deletions protobuf/proto/client.proto
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,22 @@ message ClientToServerCmd {
message DelComment {
string comment_id = 1;
}

message AddSubtitle {
string media_file_id = 1;
string file_name = 2;
string contents_base64 = 3;
}
message EditSubtitleInfo {
string id = 1;
optional string title = 2;
optional string language_code = 3;
optional float time_offset = 4;
}
message DelSubtitle {
string id = 1;
}

message ListMyMessages {
}
message JoinCollab {
Expand All @@ -107,6 +123,7 @@ message ClientToServerCmd {
bool loop = 2;
double seek_time_sec = 3;
optional string drawing = 4;
optional string subtitle_id = 5;
}
message OrganizerCmd {
string cmd = 1;
Expand All @@ -126,19 +143,30 @@ message ClientToServerCmd {

oneof cmd {
OpenNavigationPage open_navigation_page = 10;

OpenMediaFile open_media_file = 20;
DelMediaFile del_media_file = 30;
RenameMediaFile rename_media_file = 40;

AddComment add_comment = 50;
EditComment edit_comment = 60;
DelComment del_comment = 70;

AddSubtitle add_subtitle = 75;
EditSubtitleInfo edit_subtitle_info = 76;
DelSubtitle del_subtitle = 77;

ListMyMessages list_my_messages = 80;

JoinCollab join_collab = 90;
LeaveCollab leave_collab = 100;
CollabReport collab_report = 110;

OrganizerCmd organizer_cmd = 120;

MoveToFolder move_to_folder = 130;
ReorderItems reorder_items = 140;

Logout logout = 150;
}
}
9 changes: 5 additions & 4 deletions protobuf/proto/common.proto
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,11 @@ message Subtitle {
string media_file_id = 2;
string title = 3;
string language_code = 4;
optional string playback_url = 5;
string orig_filename = 6;
optional google.protobuf.Timestamp added_time = 7;
float time_offset = 8; // Can be negative
string orig_filename = 5;
float time_offset = 6; // Can be negative
string orig_url = 7;
string playback_url = 8;
optional google.protobuf.Timestamp added_time = 20;
}

// ---------------------------------------------------------
Expand Down
1 change: 1 addition & 0 deletions protobuf/proto/organizer.proto
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ message AuthzUserActionRequest {
RENAME = 1;
DELETE = 2;
COMMENT = 3;
EDIT = 4;
}
MediaFile media_file = 1;
Op op = 2;
Expand Down
Loading

0 comments on commit 4629dbc

Please sign in to comment.