Skip to content

Commit

Permalink
Implement Thumbnail Preview feature on frontend
Browse files Browse the repository at this point in the history
  • Loading branch information
gkourie committed Jan 30, 2024
1 parent bbea48c commit 1b6c587
Show file tree
Hide file tree
Showing 14 changed files with 262 additions and 11 deletions.
45 changes: 35 additions & 10 deletions app/Http/Controllers/Views/Videos/VideoController.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,15 +72,40 @@ public function show(Request $request, $id)
'too-large' => VIDEO::ERROR_TOO_LARGE,
]);

return view('videos.show', compact(
'user',
'video',
'volume',
'videos',
'shapes',
'labelTrees',
'annotationSessions',
'errors'
));
$fileIds = $volume->orderedFiles()->pluck('uuid', 'id');

if ($volume->isImageVolume()) {
$thumbUriTemplate = thumbnail_url(':uuid');
} else {
$thumbUriTemplate = thumbnail_url(':uuid', config('videos.thumbnail_storage_disk'));
}

$spritesThumbnailsPerSprite = config('videos.sprites_thumbnails_per_sprite');
$spritesThumbnailInterval = config('videos.sprites_thumbnail_interval');
$spritesMaxThumbnails = config('videos.sprites_max_thumbnails');
$spritesMinThumbnails = config('videos.sprites_min_thumbnails');
$spritesThumbnailWidth = config('videos.sprites_thumbnail_width');
$spritesThumbnailHeight = config('videos.sprites_thumbnail_height');
return view(
'videos.show',
compact(
'user',
'video',
'volume',
'videos',
'shapes',
'labelTrees',
'annotationSessions',
'errors',
'fileIds',
'thumbUriTemplate',
'spritesThumbnailsPerSprite',
'spritesThumbnailInterval',
'spritesMaxThumbnails',
'spritesMinThumbnails',
'spritesThumbnailWidth',
'spritesThumbnailHeight'
)
);
}
}
30 changes: 30 additions & 0 deletions resources/assets/js/videos/components/scrollStrip.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,19 @@
:style="scrollerStyle"
@mousemove="handleUpdateHoverTime"
>
<thumbnail-preview
:duration="duration"
:hoverTime="hoverTime"
:clientMouseX="clientMouseX"
:scrollstripTop="scrollstripTop"
v-if="showThumb && showThumbPreview"
></thumbnail-preview>
<video-progress
:duration="duration"
:element-width="elementWidth"
@seek="emitSeek"
@mousemove="handleVideoProgressMousemove"
@mouseout="hideThumbnailPreview"
></video-progress>
<div class="annotation-tracks-wrapper">
<annotation-tracks
Expand Down Expand Up @@ -52,11 +61,13 @@ import AnnotationTracks from './annotationTracks';
import Events from '../../core/events';
import Keyboard from '../../core/keyboard';
import VideoProgress from './videoProgress';
import ThumbnailPreview from './thumbnailPreview';
export default {
components: {
videoProgress: VideoProgress,
annotationTracks: AnnotationTracks,
thumbnailPreview: ThumbnailPreview,
},
props: {
tracks: {
Expand All @@ -77,6 +88,10 @@ export default {
type: Boolean,
default: false,
},
showThumbnailPreview: {
type: Boolean,
default: true
}
},
data() {
return {
Expand All @@ -91,6 +106,10 @@ export default {
hoverTime: 0,
hasOverflowTop: false,
hasOverflowBottom: false,
// thumbnail preview
clientMouseX: 0,
scrollstripTop: 0,
showThumb: false,
};
},
computed: {
Expand Down Expand Up @@ -137,6 +156,9 @@ export default {
hasOverflowRight() {
return this.elementWidth + this.scrollLeft > this.initialElementWidth;
},
showThumbPreview() {
return this.showThumbnailPreview && this.showThumb;
}
},
methods: {
updateInitialElementWidth() {
Expand Down Expand Up @@ -210,6 +232,14 @@ export default {
this.hasOverflowTop = false;
this.hasOverflowBottom = false;
},
handleVideoProgressMousemove(clientX) {
this.showThumb = true;
this.clientMouseX = clientX;
this.scrollstripTop = this.$refs.scroller.getBoundingClientRect().top;
},
hideThumbnailPreview() {
this.showThumb = false;
},
},
watch: {
hoverTime(time) {
Expand Down
12 changes: 12 additions & 0 deletions resources/assets/js/videos/components/settingsTab.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export default {
'showLabelTooltip',
'showMousePosition',
'showProgressIndicator',
'showThumbnailPreview'
],
annotationOpacity: 1,
showMinimap: true,
Expand All @@ -23,6 +24,7 @@ export default {
showMousePosition: false,
playbackRate: 1.0,
showProgressIndicator: true,
showThumbnailPreview: true,
};
},
methods: {
Expand Down Expand Up @@ -50,6 +52,12 @@ export default {
handleHideProgressIndicator() {
this.showProgressIndicator = false;
},
handleShowThumbnailPreview() {
this.showThumbnailPreview = true;
},
handleHideThumbnailPreview() {
this.showThumbnailPreview = false;
},
},
watch: {
annotationOpacity(value) {
Expand Down Expand Up @@ -86,6 +94,10 @@ export default {
this.$emit('update', 'showProgressIndicator', show);
Settings.set('showProgressIndicator', show);
},
showThumbnailPreview(show) {
this.$emit('update', 'showThumbnailPreview', show);
Settings.set('showThumbnailPreview', show);
},
},
created() {
this.restoreKeys.forEach((key) => {
Expand Down
141 changes: 141 additions & 0 deletions resources/assets/js/videos/components/thumbnailPreview.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
<template>
<div
class="thumbnail-preview"
ref="thumbnailPreview"
:width="thumbnailWidth"
:height="thumbnailHeight">

<canvas
class="thumbnail-canvas"
ref="thumbnailCanvas"
:width="thumbnailWidth"
:height="thumbnailHeight"
></canvas>
</div>
</template>

<script>
let transformUuid = function (uuid) {
return uuid[0] + uuid[1] + '/' + uuid[2] + uuid[3] + '/' + uuid;
};
export default {
props: {
duration: {
type: Number,
required: true,
},
hoverTime: {
type: Number,
required: true,
},
clientMouseX: {
type: Number,
required: true,
},
scrollstripTop: {
type: Number,
required: true,
},
},
data() {
return {
thumbnailPreview: null,
thumbnailCanvas: null,
sprite: new Image(),
spriteIdx: 0,
thumbProgressBarSpace: 150,
spritesFolderPath: null,
spriteNotFound: false,
// default values but will be overwritten in created()
thumbnailWidth: 240,
thumbnailHeight: 138,
thumbnailsPerSprite: 25,
thumbnailInterval: 2.5,
estimatedThumbnails: 0,
};
},
methods: {
updateSprite() {
this.spriteIdx = Math.floor(this.hoverTime / (this.thumbnailInterval * this.thumbnailsPerSprite));
this.sprite.src = this.spritesFolderPath + "sprite_" + this.spriteIdx + ".webp";
},
viewThumbnailPreview() {
// calculate the current row and column of the sprite
let thumbnailIndex = Math.floor(this.hoverTime / this.thumbnailInterval) % this.thumbnailsPerSprite;
if (this.hoverTime >= this.durationRounded) {
thumbnailIndex = thumbnailIndex === 0 ? this.thumbnailsPerSprite - 1 : this.estimatedThumbnails - 1;
}
let thumbnailRow = Math.floor(thumbnailIndex / Math.sqrt(this.thumbnailsPerSprite));
let thumbnailColumn = thumbnailIndex % Math.sqrt(this.thumbnailsPerSprite);
// calculate the x and y coordinates of the sprite sheet
let sourceX = this.thumbnailWidth * thumbnailColumn;
let sourceY = this.thumbnailHeight * thumbnailRow;
// draw the current thumbnail to the canvas
let context = this.thumbnailCanvas.getContext('2d');
context.clearRect(0, 0, this.thumbnailCanvas.width, this.thumbnailCanvas.height);
context.drawImage(this.sprite, sourceX, sourceY, this.thumbnailWidth, this.thumbnailHeight, 0, 0, this.thumbnailCanvas.width, this.thumbnailCanvas.height);
// position the thumbnail preview based on the mouse position
let sideButtonsWidth = 52;
let left = Math.min(this.clientMouseX - this.thumbnailCanvas.width / 2, window.innerWidth - this.thumbnailCanvas.width - sideButtonsWidth);
this.thumbnailPreview.style.left = left + 'px';
this.thumbnailPreview.style.top = (this.scrollstripTop - this.thumbProgressBarSpace) + 'px';
},
updateThumbnailInterval() {
let maxThumbnails = biigle.$require('videos.spritesMaxThumbnails');
let minThumbnails = biigle.$require('videos.spritesMinThumbnails');
let defaultThumbnailInterval = biigle.$require('videos.spritesThumbnailInterval');
this.durationRounded = Math.floor(this.duration * 10) / 10;
this.estimatedThumbnails = Math.floor(this.durationRounded / defaultThumbnailInterval);
if (this.estimatedThumbnails > maxThumbnails) {
this.estimatedThumbnails = maxThumbnails;
this.thumbnailInterval = this.durationRounded / maxThumbnails;
} else if (this.estimatedThumbnails < minThumbnails) {
this.estimatedThumbnails = minThumbnails;
this.thumbnailInterval = this.durationRounded / minThumbnails;
} else {
this.thumbnailInterval = defaultThumbnailInterval;
}
},
setSpritesFolderpath() {
let fileUuids = biigle.$require('videos.fileUuids');
let thumbUri = biigle.$require('videos.thumbUri');
let videoid = biigle.$require('videos.id');
let fileUuid = fileUuids[videoid];
this.spritesFolderPath = thumbUri.replace(':uuid', transformUuid(fileUuid) + '/').replace('.jpg', '');
},
},
watch: {
hoverTime() {
this.updateSprite();
},
},
created() {
this.setSpritesFolderpath();
this.updateThumbnailInterval();
this.thumbnailWidth = biigle.$require('videos.spritesThumbnailWidth');
this.thumbnailHeight = biigle.$require('videos.spritesThumbnailHeight');
this.thumbnailsPerSprite = biigle.$require('videos.spritesThumbnailsPerSprite');
},
mounted() {
this.thumbnailPreview = this.$refs.thumbnailPreview;
this.thumbnailCanvas = this.$refs.thumbnailCanvas;
this.updateSprite();
this.sprite.onload = () => {
this.viewThumbnailPreview();
}
// can't hide the error 404 message on the browser console
// trying to use a http request to ask if the file exists and wrap it with try/catch
// does prevent the GET 404(Not Found) error
// but we get a HEAD 404(Not Found) error instead (maybe server side?)
this.sprite.onerror = () => {
if (this.thumbnailPreview.style.display !== 'none') {
this.thumbnailPreview.style.display = 'none';
}
}
}
};
</script>
12 changes: 12 additions & 0 deletions resources/assets/js/videos/components/videoProgress.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
<div
class="video-progress"
@click="emitSeek"
@mousemove="emitMousemove"
@mouseout="emitMouseout"
>
<tick
v-for="time in ticks"
Expand Down Expand Up @@ -31,6 +33,7 @@ export default {
data() {
return {
tickSpacing: 100,
lastClientX: 0,
};
},
computed: {
Expand All @@ -55,6 +58,15 @@ export default {
emitSeek(e) {
this.$emit('seek', (e.clientX - e.target.getBoundingClientRect().left) / e.target.clientWidth * this.duration);
},
emitMousemove(e) {
if (e.clientX !== this.lastClientX) {
this.lastClientX = e.clientX;
this.$emit('mousemove', e.clientX);
}
},
emitMouseout() {
this.$emit('mouseout');
},
},
};
</script>
5 changes: 5 additions & 0 deletions resources/assets/js/videos/components/videoTimeline.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
:duration="duration"
:current-time="currentTime"
:seeking="seeking"
:showThumbnailPreview="showThumbnailPreview"
@seek="emitSeek"
@select="emitSelect"
@deselect="emitDeselect"
Expand Down Expand Up @@ -65,6 +66,10 @@ export default {
return null;
},
},
showThumbnailPreview: {
type: Boolean,
default: true,
},
},
data() {
return {
Expand Down
3 changes: 2 additions & 1 deletion resources/assets/js/videos/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import './filters/videoTime';
import Navbar from './navbar';
import SearchResults from './searchResults';
import VideoContainer from './videoContainer';

import ThumbnailPreview from './components/thumbnailPreview';
biigle.$mount('search-results', SearchResults);
biigle.$mount('video-annotations-navbar', Navbar);
biigle.$mount('video-container', VideoContainer);
biigle.$mount('thumbnail-preview', ThumbnailPreview);
1 change: 1 addition & 0 deletions resources/assets/js/videos/stores/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ let defaults = {
showLabelTooltip: false,
showMousePosition: false,
showProgressIndicator: true,
showThumbnailPreview: true,
};

export default new Settings({
Expand Down
1 change: 1 addition & 0 deletions resources/assets/js/videos/videoContainer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export default {
showMousePosition: false,
playbackRate: 1.0,
showProgressIndicator: true,
showThumbnailPreview: true,
},
openTab: '',
urlParams: {
Expand Down
Loading

0 comments on commit 1b6c587

Please sign in to comment.