Skip to content

Commit

Permalink
Add autoplay for transcript click events; clean up transcript.ts
Browse files Browse the repository at this point in the history
  • Loading branch information
carlobortolan committed Nov 12, 2024
1 parent 370c59e commit 00733ba
Show file tree
Hide file tree
Showing 5 changed files with 61 additions and 71 deletions.
13 changes: 3 additions & 10 deletions web/template/partial/stream/transcript.gohtml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{{define "transcript-modal"}}
<div x-data="{transcriptController: new watch.TranscriptController(), isOutOfSync: false, showScrollUp: false}"
<div x-data="{ transcriptController: new watch.TranscriptController(), isOutOfSync: false, showScrollUp: false }"
class="relative h-full border rounded-lg dark:border-gray-800 flex flex-col items-center justify-center overflow-hidden">
<div class="h-full w-full max-w-3xl flex flex-col">
<div class="flex justify-between items-end p-4">
Expand All @@ -16,27 +16,20 @@
<div class="flex-grow overflow-hidden relative">
{{template "transcript-list" .}}
</div>
<!--<div class="w-full flex items-center justify-center" x-show="isOutOfSync">
<button type="button" title="Sync to video time"
class="text-3 text-sm font-semibold hover:bg-blue-500 dark:bg-indigo-600 rounded-full p-1 my-3 w-1/2 max-w-3xl"
@click="isOutOfSync = false;">
Sync to video time
</button>
</div>-->
</div>
</div>
{{end}}

{{define "transcript-list"}}
<div x-cloak
x-data="{ transcriptController: new watch.TranscriptController(), transcript: [] }"
x-init="() => { transcriptController.init('transcript-list', $el);}"
x-init="() => { transcriptController.init('transcript-list', $el); }"
@update="(e) => (transcript = e.detail)"
class="h-full w-full max-w-3xl mx-auto transcript-container overflow-hidden">
<div class="relative grid gap-1 overflow-y-auto pr-3 h-full">
<template x-for="(cue, index) in transcript" :key="index">
<div class="flex items-start space-x-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 p-2 rounded"
@click="watch.jumpTo({Ms: cue.startTime * 1000});" :data-cue-start="cue.startTime">
@click="watch.jumpTo({ Ms: cue.startTime * 1000 }, true);" :data-cue-start="cue.startTime">
<div class="text-xs text-gray-500 dark:text-gray-400 mt-1">
<span x-text="new Date(cue.startTime * 1000).toISOString().substr(11, 8)"></span>
</div>
Expand Down
7 changes: 4 additions & 3 deletions web/template/watch.gohtml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
<script defer src="/static/node_modules/katex/dist/contrib/copy-tex.min.js"></script>
{{end}}
</head>
<body x-data="{'streamID': {{$stream.Model.ID}}, seekLogger: new watch.SeekLogger('{{$stream.ID}}'), sidebar: $persist(watch.SidebarState.Hidden).as('sidebarState'), showShare: false}"
<body x-data="{'streamID': {{$stream.Model.ID}}, seekLogger: new watch.SeekLogger('{{$stream.ID}}'), sidebar: $persist(watch.SidebarState.Hidden).as('sidebarState'), showShare: false, transcriptAvailable: false}"
x-init="seekLogger.attach();">
{{template "header" .IndexData.TUMLiveContext}}
<div id="shortcuts-help-modal" class="hidden flex fixed top-0 h-screen w-screen z-50 backdrop-brightness-50">
Expand Down Expand Up @@ -425,7 +425,7 @@
{{end}}

<!-- Transcript -->
<div id="transcript-desktop" x-cloak="" x-show="sidebar === watch.SidebarState.Transcript"
<div id="transcript-desktop" x-cloak="" x-show="sidebar === watch.SidebarState.Transcript && transcriptAvailable"
:class="sidebar === watch.SidebarState.Transcript ? 'lg:basis-1/4' : 'lg:basis-0'"
class="basis-full md:h-16/9 lg:h-16/6 px-2 lg:order-none order-4">
{{template "transcript-modal" $stream.ID}}
Expand Down Expand Up @@ -497,7 +497,8 @@
</button>
{{end}}

<button @click="sidebar = (sidebar === watch.SidebarState.Transcript ? watch.SidebarState.Hidden : watch.SidebarState.Transcript)"
<!-- Transcript Button -->
<button x-show="transcriptAvailable" @toggletranscript.window="e => {transcriptAvailable=true}" @click="sidebar = (sidebar === watch.SidebarState.Transcript ? watch.SidebarState.Hidden : watch.SidebarState.Transcript)"
class="rounded-lg px-3 py-1 md:px-4 py-2 h-fit w-fit bg-gray-100 hover:bg-gray-200 dark:bg-secondary-light dark:hover:bg-gray-600"
:title="'Show Transcript'">
<i class="fa-solid fa-text-height text-4"></i>
Expand Down
5 changes: 4 additions & 1 deletion web/ts/TUMLiveVjs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -635,7 +635,7 @@ export type jumpToSettings = {
S: number | undefined;
};

export function jumpTo(settings: jumpToSettings) {
export function jumpTo(settings: jumpToSettings, autoplay = false) {
if (settings.timeParts) {
settings.time = new Time(settings.timeParts.hours, settings.timeParts.minutes, settings.timeParts.seconds);
} else if (settings.Ms) {
Expand All @@ -646,6 +646,9 @@ export function jumpTo(settings: jumpToSettings) {
for (let j = 0; j < players.length; j++) {
players[j].ready(() => {
players[j].currentTime(settings.time.toSeconds());
if (autoplay && players[j].paused()) {
players[j].play();
}
});
}
}
Expand Down
3 changes: 2 additions & 1 deletion web/ts/track-bars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ export async function loadAndSetTrackbars(player: VideoJsPlayer, streamID: numbe
for (const language of LANGUAGES) {
await fetch(`/api/stream/${streamID}/subtitles/${language.id}`).then((res) => {
if (res.ok) {
window.dispatchEvent(new CustomEvent("togglesearch", { detail: { streamID: streamID } }));
window.dispatchEvent(new CustomEvent("togglesearch", { detail: { streamID: streamID } })); // Used for enabling watch searchbar
window.dispatchEvent(new CustomEvent("toggletranscript", { detail: { streamID: streamID } })); // Used for enabling button to show transcript-modal
player.addRemoteTextTrack(
{
src: `/api/stream/${streamID}/subtitles/${language.id}`,
Expand Down
104 changes: 48 additions & 56 deletions web/ts/transcript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,23 @@ import { getPlayers } from "./TUMLiveVjs";
import { VideoJsPlayer } from "video.js";

export class TranscriptController {
static initiatedInstances: Map<string, Promise<TranscriptController>> = new Map<
string,
Promise<TranscriptController>
>();
static initiatedInstances: Map<string, Promise<TranscriptController>> = new Map();

private list: VTTCue[];
private elem: HTMLElement;
private lastSyncTime: number;
private player: VideoJsPlayer;
private selectedTrackLabel: string;

reset(): void {
this.player = getPlayers()[0];
}

constructor() {
this.lastSyncTime = 0;
this.selectedTrackLabel = "English";
}

reset(): void {
this.player = getPlayers()[0];
}

async init(key: string, element: HTMLElement) {
if (TranscriptController.initiatedInstances[key]) {
(await TranscriptController.initiatedInstances[key]).unsub();
Expand All @@ -35,22 +32,19 @@ export class TranscriptController {
}

async syncTranscript() {
const transcriptDesktop = document.getElementById('transcript-desktop');
const transcriptDesktop = document.getElementById("transcript-desktop");
if (!this.elem || !transcriptDesktop || transcriptDesktop.offsetParent === null) {
return;
}

const now = Date.now();
if (now - this.lastSyncTime < 1000) {
// Sync once every second
if (now - this.lastSyncTime < 1000 || this.player.paused()) {
return;
}
this.lastSyncTime = now;

if (this.player.paused()) {
return;
}

console.debug("Syncing transcript...");

const currentTime = this.player.currentTime();
const transcript = await this.fetchTranscript(this.player);
this.updateTranscript(transcript);
Expand All @@ -62,57 +56,56 @@ export class TranscriptController {
let transcript: VTTCue[] = [];

// Try to find the selected track first
for (let i = 0; i < textTracks.length; i++) {
const track = textTracks[i];
if ((track.kind === "captions" || track.kind === "subtitles") && track.label === this.selectedTrackLabel) {
for (let j = 0; j < track.cues.length; j++) {
const cue = track.cues[j] as VTTCue;
transcript.push(cue);
}
if (transcript.length > 0) {
return transcript;
}
}
transcript = this.getTranscriptFromTracks(textTracks, this.selectedTrackLabel);
if (transcript.length > 0) {
return transcript;
}

// If no selected track is found, use other available subtitles
transcript = this.getTranscriptFromTracks(textTracks);
return transcript;
}

getTranscriptFromTracks(textTracks: TextTrackList, label?: string): VTTCue[] {
let transcript: VTTCue[] = [];
for (let i = 0; i < textTracks.length; i++) {
const track = textTracks[i];
if (track.kind === "captions" || track.kind === "subtitles") {
if ((track.kind === "captions" || track.kind === "subtitles") && (!label || track.label === label)) {
for (let j = 0; j < track.cues.length; j++) {
const cue = track.cues[j] as VTTCue;
transcript.push(cue);
}
if (label && transcript.length > 0) {
return transcript;
}
}
}

return transcript;
}

updateTranscript(transcript: VTTCue[]) {
this.list = transcript;
const event = new CustomEvent('update', { detail: transcript });
const event = new CustomEvent("update", { detail: transcript });
this.elem.dispatchEvent(event);
}

highlightActiveCue(currentTime: number) {
const activeCue = this.list.find(cue => cue.startTime <= currentTime && cue.endTime >= currentTime);
const cueElements = this.elem.querySelectorAll('[data-cue-start]');
const activeCue = this.list.find((cue) => cue.startTime <= currentTime && cue.endTime >= currentTime);
const cueElements = this.elem.querySelectorAll("[data-cue-start]");
cueElements.forEach((cueElement: HTMLElement) => {
cueElement.classList.remove('bg-blue-100', 'dark:bg-blue-700');
cueElement.classList.remove("bg-blue-100", "dark:bg-blue-700");
});

if (activeCue) {
const cueElement = this.elem.querySelector(`[data-cue-start="${activeCue.startTime}"]`);
if (cueElement) {
cueElement.classList.add('bg-blue-100', 'dark:bg-blue-700');
cueElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
cueElement.classList.add("bg-blue-100", "dark:bg-blue-700");
cueElement.scrollIntoView({ behavior: "smooth", block: "center" });
}
}
}

onUpdate(data: any) {
// Process the data and update the transcript list
this.updateTranscript(data);
}

Expand All @@ -123,43 +116,42 @@ export class TranscriptController {
async downloadTranscript() {
const player = getPlayers()[0];
const textTracks = player.textTracks();
let transcript = "";
let transcript = this.getTranscriptText(textTracks, this.selectedTrackLabel);

// If no selected track is found, use other available subtitles
if (transcript === "") {
transcript = this.getTranscriptText(textTracks);
}

// Iterate over the text tracks to find the selected track
this.downloadTextAsFile(transcript, "transcript.txt");
}

getTranscriptText(textTracks: TextTrackList, label?: string): string {
let transcript = "";
for (let i = 0; i < textTracks.length; i++) {
const track = textTracks[i];
if ((track.kind === "captions" || track.kind === "subtitles") && track.label === this.selectedTrackLabel) {
// Iterate over the cues to extract the transcript text
if ((track.kind === "captions" || track.kind === "subtitles") && (!label || track.label === label)) {
for (let j = 0; j < track.cues.length; j++) {
const cue = track.cues[j];
transcript += `${cue.text}\n\n`;
}
}
}

// If no selected track is found, use other available subtitles
if (transcript === "") {
for (let i = 0; i < textTracks.length; i++) {
const track = textTracks[i];
if (track.kind === "captions" || track.kind === "subtitles") {
// Iterate over the cues to extract the transcript text
for (let j = 0; j < track.cues.length; j++) {
const cue = track.cues[j];
transcript += `${cue.text}\n\n`;
}
if (label && transcript !== "") {
return transcript;
}
}
}
return transcript;
}

// Create a Blob from the transcript text
const blob = new Blob([transcript], { type: "text/plain" });
downloadTextAsFile(text: string, filename: string) {
const blob = new Blob([text], { type: "text/plain" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "transcript";
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
}
}

0 comments on commit 00733ba

Please sign in to comment.