Skip to content

Commit

Permalink
Setup video.js and add video player page
Browse files Browse the repository at this point in the history
  • Loading branch information
dan-niles committed Jun 19, 2024
1 parent 11e6227 commit 660c332
Show file tree
Hide file tree
Showing 14 changed files with 504 additions and 5 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Remove old UI files and methods: template files (home.html, article.html) and `make_html_files` method in scraper.py
- Remove `--locale` arg, broken locale folder, files used for translation; translation will be restored with #222
- Create "Videos" and "Playlists" tabs for homepage in new Vue.js UI (#213, #214)
- Create video player page in new Vue.js UI (#215)
- Add support for variable playback speed in video player (#174)

## [2.3.0] - 2024-05-22

Expand Down
25 changes: 25 additions & 0 deletions zimui/cypress/e2e/video_player_page.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
describe('video player page', () => {
beforeEach(() => {
cy.intercept('GET', '/channel.json', { fixture: 'channel/channel.json' }).as('getChannel')
cy.intercept('GET', '/playlists/uploads_from_openzim_testing-917Q.json', {
fixture: 'channel/playlists/uploads_from_openzim_testing-917Q.json'
}).as('getUploads')
cy.visit('/')
cy.wait('@getChannel')
cy.wait('@getUploads')
})

it('loads the video and related information', () => {
cy.intercept('GET', '/videos/timelapse-9Tgo.json', {
fixture: 'channel//videos/timelapse-9Tgo.json'
}).as('getVideo')
cy.contains('.v-card-title ', 'Timelapse').click()
cy.wait('@getVideo')

cy.url().should('include', '/watch')
cy.contains('.video-title', 'Timelapse')
cy.contains('.video-date', 'Published on Jun 4, 2024')
cy.contains('.video-channel', 'openZIM_testing')
cy.contains('.video-description', 'This is a short video of a timelapse.')
})
})
19 changes: 19 additions & 0 deletions zimui/cypress/fixtures/channel/videos/timelapse-9Tgo.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"id": "9TgosbGRsTk",
"title": "Timelapse",
"description": "This is a short video of a timelapse.",
"author": {
"channelId": "UC8elThf5TGMpQfQc_VE917Q",
"channelTitle": "openZIM_testing",
"channelDescription": "",
"channelJoinedDate": "2024-06-04T13:30:16.232286Z",
"profilePath": "channels/UC8elThf5TGMpQfQc_VE917Q/profile.jpg",
"bannerPath": "channels/UC8elThf5TGMpQfQc_VE917Q/banner.jpg"
},
"publicationDate": "2024-06-04T14:57:45Z",
"videoPath": "videos/9TgosbGRsTk/video.webm",
"thumbnailPath": "videos/9TgosbGRsTk/video.webp",
"subtitlePath": null,
"subtitleList": [],
"duration": "PT11S"
}
4 changes: 3 additions & 1 deletion zimui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@
"axios": "^1.7.2",
"dayjs": "^1.11.11",
"pinia": "^2.1.7",
"video.js": "^8.12.0",
"vite-plugin-vuetify": "^2.0.3",
"vue": "^3.4.21",
"vue-router": "^4.3.0",
"vuetify": "^3.6.7"
"vuetify": "^3.6.7",
"webp-hero": "^0.0.2"
},
"devDependencies": {
"@mdi/font": "^7.4.47",
Expand Down
6 changes: 6 additions & 0 deletions zimui/src/App.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
<script setup lang="ts">
import { onMounted } from 'vue'
import { RouterView } from 'vue-router'
import { useMainStore } from '@/stores/main'
import { triggerWebpPolyfill } from './plugins/webp-hero'
import ErrorDisplay from '@/components/common/ErrorDisplay.vue'
const main = useMainStore()
onMounted(() => {
triggerWebpPolyfill()
})
</script>

<template>
Expand Down
49 changes: 49 additions & 0 deletions zimui/src/assets/vjs-youtube.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
.vjs-youtube,
.vjs-youtube video {
border-radius: 8px;
}

.vjs-youtube .vjs-control-bar {
border-bottom-left-radius: 8px;
border-bottom-right-radius: 8px;
}

.vjs-youtube .vjs-poster img {
border-radius: 8px;
}

.vjs-youtube .vjs-big-play-button {
background-color: rgba(0, 0, 0, 0.7);
border-radius: 1rem;
border: none;
}

.vjs-youtube .vjs-big-play-button:hover,
.vjs-youtube .vjs-big-play-button:focus,
.vjs-youtube:hover .vjs-big-play-button {
background-color: rgba(0, 0, 0, 0.8);
}

.vjs-youtube .vjs-control-bar,
.vjs-youtube .vjs-menu-button-popup .vjs-menu .vjs-menu-content,
.vjs-youtube .vjs-modal-dialog.vjs-text-track-settings {
background-color: rgba(0, 0, 0, 0.7);
}

.vjs-youtube .vjs-progress-holder {
background-color: rgba(255, 255, 255, 0.1);
}

.vjs-youtube .vjs-load-progress div {
background-color: rgb(0, 0, 0, 0.3);
}

.vjs-youtube .vjs-modal-dialog-content select {
color: rgb(190, 190, 190);
}

.vjs-youtube .vjs-track-settings-controls button {
padding-right: 5px;
padding-left: 5px;
border-radius: 8px;
}
38 changes: 38 additions & 0 deletions zimui/src/components/video/VideoPlayer.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from 'vue'
import videojs from 'video.js'
import type Player from 'video.js/dist/types/player'
import 'video.js/dist/video-js.css'
import '@/assets/vjs-youtube.css'
const props = defineProps({
options: {
type: Object,
default: () => ({})
}
})
const videoPlayer = ref<HTMLVideoElement>()
const player = ref<Player>()
// Initialize video.js when the component is mounted
onMounted(() => {
if (videoPlayer.value) {
player.value = videojs(videoPlayer.value, props.options)
}
})
// Destroy video.js when the component is unmounted
onBeforeUnmount(() => {
if (player.value) {
player.value.dispose()
}
})
</script>

<template>
<div>
<video ref="videoPlayer" class="video-js vjs-youtube"></video>
</div>
</template>
40 changes: 40 additions & 0 deletions zimui/src/components/video/player/VideoChannelInfo.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<script setup lang="ts">
import AboutDialogButton from '@/components/channel/AboutDialogButton.vue'
const props = defineProps({
profilePath: {
type: String,
required: true
},
channelTitle: {
type: String,
required: true
},
channelDescription: {
type: String,
required: true
},
joinedDate: {
type: String,
required: true
}
})
</script>

<template>
<v-row>
<v-col cols="6" class="d-flex align-center py-2">
<v-avatar size="50" class="border-thin">
<v-img :src="props.profilePath" />
</v-avatar>
<span class="video-channel ml-2 font-weight-medium">{{ props.channelTitle }}</span>
</v-col>
<v-col cols="6" class="d-flex justify-end align-center">
<about-dialog-button
:title="props.channelTitle"
:description="props.channelDescription"
:joined-date="props.joinedDate"
/>
</v-col>
</v-row>
</template>
19 changes: 19 additions & 0 deletions zimui/src/components/video/player/VideoDescription.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<script setup lang="ts">
const props = defineProps({
description: {
type: String,
required: true
}
})
</script>

<template>
<v-row>
<v-col>
<v-card class="border-thin py-2" rounded="lg" flat>
<v-card-title class="text-subtitle-1 font-weight-medium">Description</v-card-title>
<v-card-text class="video-description">{{ props.description }}</v-card-text>
</v-card>
</v-col>
</v-row>
</template>
23 changes: 23 additions & 0 deletions zimui/src/components/video/player/VideoTitleInfo.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<script setup lang="ts">
import { formatDate } from '@/utils/format-utils'
const props = defineProps({
title: {
type: String,
required: true
},
publicationDate: {
type: String,
required: true
}
})
</script>

<template>
<v-row>
<v-col class="d-flex flex-column py-2">
<h2 class="video-title">{{ props.title }}</h2>
<p class="video-date text-caption">Published on {{ formatDate(props.publicationDate) }}</p>
</v-col>
</v-row>
</template>
11 changes: 11 additions & 0 deletions zimui/src/plugins/webp-hero.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { WebpMachine, detectWebpSupport } from 'webp-hero'
import 'webp-hero/dist-cjs/polyfills.js'

export const triggerWebpPolyfill = () => {
detectWebpSupport().then((support_webp) => {
if (!support_webp) {
const webpMachine = new WebpMachine()
webpMachine.polyfillDocument()
}
})
}
2 changes: 2 additions & 0 deletions zimui/src/types/Channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export interface Channel {
export interface Author {
channelId: string
channelTitle: string
channelDescription: string
channelJoinedDate: string
profilePath?: string
bannerPath?: string
}
Expand Down
71 changes: 67 additions & 4 deletions zimui/src/views/VideoPlayerView.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
<script setup lang="ts">
import { ref, type Ref, onMounted } from 'vue'
import { ref, type Ref, onMounted, computed } from 'vue'
import { useMainStore } from '@/stores/main'
import { useRoute } from 'vue-router'
import type { Video } from '@/types/Videos'
import VideoPlayer from '@/components/video/VideoPlayer.vue'
import VideoTitleInfo from '@/components/video/player/VideoTitleInfo.vue'
import VideoChannelInfo from '@/components/video/player/VideoChannelInfo.vue'
import VideoDescription from '@/components/video/player/VideoDescription.vue'
const main = useMainStore()
const route = useRoute()
const slug: string = route.params.slug as string
Expand All @@ -28,12 +33,70 @@ const fetchData = async function () {
onMounted(() => {
fetchData()
})
const videoURL = computed<string>(() => {
return video.value?.videoPath || ''
})
const videoPoster = computed<string>(() => {
return video.value?.thumbnailPath || ''
})
const subtitles = computed(() => {
return video.value?.subtitleList.map((subtitle) => {
return {
kind: 'subtitles',
src: `${video.value?.subtitlePath}/video.${subtitle.code}.vtt`,
srclang: subtitle.code,
label: subtitle.name
}
})
})
const videoOptions = ref({
controls: true,
autoplay: false,
preload: true,
fluid: true,
responsive: true,
enableSmoothSeeking: true,
controlBar: { pictureInPictureToggle: false },
playbackRates: [0.25, 0.5, 1, 1.5, 2],
techOrder: ['html5'],
poster: videoPoster,
sources: [
{
src: videoURL
}
],
tracks: subtitles
})
</script>

<template>
<v-container v-if="video">
<h1>Video Player Page</h1>
<h2>Slug: {{ slug }}</h2>
<p>Title: {{ video.title }}</p>
<!-- Video Player -->
<v-row>
<v-spacer />
<v-col cols="12" md="8">
<video-player :options="videoOptions" />
</v-col>
<v-spacer />
</v-row>
<!-- Video Details -->
<v-row>
<v-spacer />
<v-col cols="12" md="8">
<video-title-info :title="video.title" :publication-date="video.publicationDate" />
<video-channel-info
:profile-path="video.author.profilePath || ''"
:channel-title="video.author.channelTitle || ''"
:channel-description="video.author.channelDescription || ''"
:joined-date="video.author.channelJoinedDate || ''"
/>
<video-description :description="video.description" />
</v-col>
<v-spacer />
</v-row>
</v-container>
</template>
Loading

0 comments on commit 660c332

Please sign in to comment.