Skip to content

Commit

Permalink
feat: Captions added
Browse files Browse the repository at this point in the history
  • Loading branch information
pixkk committed Aug 22, 2024
1 parent 2b85a89 commit 1601ee1
Show file tree
Hide file tree
Showing 8 changed files with 220 additions and 17 deletions.
74 changes: 71 additions & 3 deletions NUXT/components/Player/captions.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,73 @@
<template>
<v-btn fab text small disabled>
<v-icon>mdi-closed-caption-outline</v-icon>
</v-btn>

<div>
<v-bottom-sheet
v-model="sheet"
:attach="$parent.$refs.vidcontainer"
style="z-index: 777777"
scrollable
>
<template #activator="{ on, attrs }">
<v-btn
fab
text
small
color="white"
v-bind="attrs"
v-on="on"
>
<v-icon>mdi-closed-caption-outline</v-icon>
</v-btn>
</template>
<v-card class="background">
<v-subheader
v-touch="{
down: () => (sheet = false),
}"
>
Captions for current video
<v-spacer />
<v-btn fab text small @click="sheet = false">
<v-icon>mdi-close</v-icon>
</v-btn>
</v-subheader>
<v-divider />
<v-card-text
style="max-height: 50vh; flex-direction: column !important"
class="pa-0 d-flex flex-column-reverse"
>
<v-list-item-group
v-for="src in captions"
:key="src"
>
<v-list-item
@click="(sheet = false), $emit('captionsHandler', src)">
<v-list-item-content>
<v-list-item-title >
{{src.name.runs[0].text}}
</v-list-item-title>
</v-list-item-content>
</v-list-item>

<v-divider />
</v-list-item-group>

</v-card-text>
</v-card>
</v-bottom-sheet>
</div>
</template>
<script>
export default {
props: {
captions: {
type: Array,
default: [],
},
},
emits: ["captionsHandler"],
data: () => ({
sheet: false,
}),
}
</script>
57 changes: 55 additions & 2 deletions NUXT/components/Player/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,9 @@
:poster="$youtube.getThumbnail($route.query.v, '', [])"
@loadedmetadata="checkDimensions()"
@click="controlsHandler()"
/>
>
<track default kind="captions" id="captions" src="">
</video>
<audio ref="audio" mediagroup="vuetubecute" :src="audSrc" />

<!-- // TODO: merge the bottom 2 into 1 reusable component -->
Expand Down Expand Up @@ -162,7 +164,11 @@
</div>
</div>
<v-spacer />
<captions />
<captions
:captions="video.captions"
@captionsHandler="captionsHandler($event)"

/>
<loop
v-if="$refs.player"
class="mx-2"
Expand Down Expand Up @@ -353,6 +359,9 @@ import progressbar from "~/components/Player/progressbar.vue";
import sponsorblock from "~/components/Player/sponsorblock.vue";
import backType from "~/plugins/classes/backType";
import constants from "@/plugins/constants";
import { Http } from "@capacitor-community/http";
import { convertTranscriptToVTT } from "~/plugins/utils";
export default {
components: {
Expand All @@ -379,6 +388,9 @@ export default {
type: Array,
required: true,
},
captions: {
type: Array,
},
recommends: {
type: Object,
default: () => {
Expand Down Expand Up @@ -779,6 +791,36 @@ export default {
this.$refs.player.playbackRate = speed;
this.$refs.audio.playbackRate = speed;
},
async captionsHandler(q) {
if (q.baseUrl != null) {
const html = await Http.get({
url: constants.URLS.YT_MOBILE + q.baseUrl,
params: {},
}).catch((error) => error);
console.log(html);
let captions = convertTranscriptToVTT(html.data);
function textToDataURL(text) {
const blob = new Blob([text], { type: 'text/plain' });
const reader = new FileReader();
return new Promise((resolve, reject) => {
reader.onloadend = () => {
resolve(reader.result);
};
reader.onerror = reject;
reader.readAsDataURL(blob);
});
}
textToDataURL(captions).then((dataurl) => {
document.getElementById("captions").src = dataurl;
})
}
else {
document.getElementById("captions").src = "";
}
const rootElement = document.getElementById('__nuxt');
rootElement.className += "web chrome";
},
checkDimensions() {
if (this.$refs.player.videoHeight > this.$refs.player.videoWidth) {
this.isVerticalVideo = true;
Expand Down Expand Up @@ -879,4 +921,15 @@ export default {
.invisible {
opacity: 0;
}
//captions style
.chrome {
video::cue {
//font-size: 13.4px;
opacity: 1;
background-color: black;
-webkit-transform: translateY(10%) !important;
transform: translateY(10%) !important;
}
}
</style>
2 changes: 1 addition & 1 deletion NUXT/components/vidLoadRenderer.vue
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// this is an loading animation for videos
<!--this is an loading animation for videos-->
<template>
<div>
<v-sheet color="background" v-for="i in count" :key="i">
Expand Down
2 changes: 2 additions & 0 deletions NUXT/pages/watch.vue
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,7 @@ export default {
this.$youtube.getVid(this.$route.query.v).then((result) => {
// TODO: sourt "tiny" (no qualityLabel) as audio and rest as video
this.sources = result.availableResolutionsAdaptive;
this.captions = result.captions;
console.log("Video info data", result);
this.video = result;
Expand Down Expand Up @@ -520,6 +521,7 @@ export default {
showComments: false,
// share: false,
sources: [],
captions: [],
recommends: null,
loaded: false,
interval: null,
Expand Down
8 changes: 8 additions & 0 deletions NUXT/plugins/innertube.js
Original file line number Diff line number Diff line change
Expand Up @@ -750,6 +750,13 @@ class Innertube {
(content) => content.slimOwnerRenderer
)?.slimOwnerRenderer;

const captions = responseInfo.captions?.playerCaptionsTracklistRenderer?.captionTracks;
captions.unshift({
baseUrl: null,
name: {
runs: [{text: "Disable captions"}]
}
})
try {
console.log(vidMetadata.contents);
this.playerParams =
Expand Down Expand Up @@ -819,6 +826,7 @@ class Innertube {
ownerData.navigationEndpoint
),
channelImg: ownerData?.thumbnail?.thumbnails[0].url,
captions: captions,
availableResolutions: resolutions?.formats,
availableResolutionsAdaptive: resolutions?.adaptiveFormats,
metadata: {
Expand Down
71 changes: 65 additions & 6 deletions NUXT/plugins/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,70 @@ function setHttp(link) {

// Replace inputted html with tweemoji
function parseEmoji(body) {
if (twemoji)
return twemoji.parse(body, {
folder: "svg",
ext: ".svg",
base: 'https://cdn.jsdelivr.net/gh/twitter/[email protected]/assets/'
});
try {

if (twemoji)
return twemoji.parse(body, {
folder: "svg",
ext: ".svg",
base: 'https://cdn.jsdelivr.net/gh/twitter/[email protected]/assets/'
});
}catch (e) {

}
}

// Function to convert seconds to VTT timestamp format
function secondsToVTTTime(seconds) {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = Math.floor(seconds % 60);
const milliseconds = Math.round((seconds % 1) * 1000);

return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(secs).padStart(2, '0')}.${String(milliseconds).padStart(3, '0')}`;
}
function decodeHtmlEntities(str) {
const parser = new DOMParser();
const doc = parser.parseFromString(`<!doctype html><body>${str}`, 'text/html');
return doc.body.textContent || '';
}

// Function to parse transcript and convert to VTT
function convertTranscriptToVTT(transcript) {
// transcript =JSON.parse(JSON.stringify(transcript)).data;
console.warn(transcript);
// Extract <text> elements from the transcript
const textElements = transcript.match(/<text start="([\d.]+)" dur="([\d.]+)">([^<]+)<\/text>/g);

console.warn(textElements);
// Initialize VTT output with header
let vttOutput = 'WEBVTT\n\n';
for (let i = 0; i < textElements.length; i++) {
let textElement = textElements[i];
const startMatch = textElement.match(/start="([\d.]+)"/);
const durMatch = textElement.match(/dur="([\d.]+)"/);
const contentMatch = textElement.match(/>([^<]+)<\/text>/);

if (startMatch && durMatch && contentMatch) {
const start = parseFloat(startMatch[1]);
const duration = parseFloat(durMatch[1]);
const content = decodeHtmlEntities(contentMatch[1].replace(/\+/g, ' ')); // Decode HTML entities

let end;
if (i+1 >= textElements.length) {
end = start + duration;
}
else {
end = textElements[i+1].match(/start="([\d.]+)"/)[1];
}
const startTime = secondsToVTTTime(start);
const endTime = secondsToVTTTime(end);

vttOutput += `${startTime} --> ${endTime}\n\n\n`; // margin bottom huh
vttOutput += `${startTime} --> ${endTime}\n${content}\n\n`;
}
}
return vttOutput.trim();
}

function linkParser(url) {
Expand Down Expand Up @@ -93,4 +151,5 @@ module.exports = {
delay,
parseEmoji,
humanFileSize,
convertTranscriptToVTT
};
20 changes: 16 additions & 4 deletions NUXT/plugins/vuetube.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,12 +107,24 @@ const module = {
return true;
},

resetBackActions() {
backActions.reset();
async resetBackActions() {
try {
backActions.reset();

} catch (e) {
await this.launchBackHandling();
backActions.reset();
}
},

addBackAction(action) {
backActions.addAction(action);
async addBackAction(action) {
try {
backActions.addAction(action);

} catch (e) {
await this.launchBackHandling();
backActions.addAction(action);
}
},

back(listenerFunc) {
Expand Down
3 changes: 2 additions & 1 deletion NUXT/plugins/youtube.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,8 @@ const innertubeModule = {
try {
return await InnertubeAPI.VidInfoAsync(id);
} catch (error) {
console.error(error);
await this.getAPI();
return await InnertubeAPI.VidInfoAsync(id);
}
},

Expand Down

0 comments on commit 1601ee1

Please sign in to comment.