Skip to content

Commit

Permalink
Add video thumbnail previews and ServiceWorker caching
Browse files Browse the repository at this point in the history
  • Loading branch information
remixz committed Apr 8, 2017
1 parent 1ea372f commit 87233cb
Show file tree
Hide file tree
Showing 12 changed files with 194 additions and 18 deletions.
8 changes: 7 additions & 1 deletion build/webpack.prod.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ var merge = require('webpack-merge')
var baseWebpackConfig = require('./webpack.base.conf')
var HtmlWebpackPlugin = require('html-webpack-plugin')
var ExtractTextPlugin = require('extract-text-webpack-plugin')
var OfflinePlugin = require('offline-plugin')
var env = config.build.env

var webpackConfig = merge(baseWebpackConfig, {
Expand Down Expand Up @@ -75,7 +76,12 @@ var webpackConfig = merge(baseWebpackConfig, {
}),
new webpack.LoaderOptionsPlugin({
minimize: true
})
}),
new OfflinePlugin({
ServiceWorker: {
events: true
}
})
]
})

Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"dependencies": {
"animejs": "^2.0.1",
"axios": "^0.16.0",
"base64-js": "^1.2.0",
"date-fns": "^1.27.2",
"scriptjs": "^2.5.8",
"socket.io-client": "^1.7.2",
Expand Down Expand Up @@ -55,6 +56,7 @@
"function-bind": "^1.1.0",
"html-webpack-plugin": "^2.28.0",
"http-proxy-middleware": "^0.17.3",
"offline-plugin": "^4.6.2",
"opn": "^4.0.2",
"ora": "^1.1.0",
"semver": "^5.3.0",
Expand Down
22 changes: 22 additions & 0 deletions server/bif.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const axios = require('axios')
const url = require('url')
const qs = require('querystring')

function bifHandler (req, res) {
res.setHeader('Access-Control-Allow-Origin', '*')

const {query} = url.parse(req.url)
const {bif} = qs.parse(query)
if (!bif) return res.end()
const {hostname} = url.parse(bif)
if (!hostname.endsWith('crunchyroll.com')) return res.end()

res.setHeader('Content-Type', 'application/octet-stream')
axios.get(bif, {
responseType: 'stream'
})
.then(({data}) => data.pipe(res))
.catch(() => res.end())
}

module.exports = bifHandler
14 changes: 14 additions & 0 deletions server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,18 @@ const http = require('http')
const compress = require('compression')()
const autocompleteHandler = require('./autocomplete')
const openingHandler = require('./opening')
const bifHandler = require('./bif')

const srv = http.createServer((req, res) => {
compress(req, res, () => {
if (req.url.indexOf('/autocomplete') === 0) {
autocompleteHandler(req, res)
} else if (req.url.indexOf('/opening') === 0) {
openingHandler(req, res)
} else if (req.url.indexOf('/bif') === 0) {
bifHandler(req, res)
} else if (req.url.indexOf('/update') === 0) {
updateHandler(req, res)
} else {
res.end('love arrow shoot!')
}
Expand All @@ -21,6 +26,15 @@ function findRoom (rooms) {
return Object.keys(rooms).find((k) => k.startsWith('umi//'))
}

function updateHandler (req, res) {
const token = req.url.split('/update/')[1]
if (token !== process.env.UPDATE_TOKEN) {
return res.end('not ok')
}
io.emit('app-update')
res.end('ok')
}

io.on('connection', (socket) => {
socket.on('join-room', (room) => {
const currentRoom = findRoom(socket.rooms)
Expand Down
8 changes: 6 additions & 2 deletions server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,13 @@
"socket.io": "^1.7.2"
},
"scripts": {
"start": "node index.js"
"start": "node index.js",
"dev": "cross-env UPDATE_TOKEN=example nodemon index.js"
},
"now": {
"alias": "umi-watch-api"
"alias": "umi-watch-api",
"env": {
"UPDATE_TOKEN": "@update-token"
}
}
}
23 changes: 20 additions & 3 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,17 @@
</transition>
</main>
<footer class="mw8 center relative">
<a href="https://www.netlify.com" target="_blank" class="absolute right-0">
<a href="https://www.netlify.com" target="_blank" rel="noopener" class="absolute right-0">
<img src="https://www.netlify.com/img/global/badges/netlify-light.svg"/>
</a>
<p class="gray">
This site is not endorsed by or affiliated with Crunchyroll. <br /> Created by <a href="https://twitter.com/zachbruggeman" target="_blank">Zach Bruggeman</a>. <a href="https://github.com/remixz/umi" target="_blank">View source on GitHub</a>.
This site is not endorsed by or affiliated with Crunchyroll. <br /> Created by <a href="https://twitter.com/zachbruggeman" target="_blank" rel="noopener">Zach Bruggeman</a>. <a href="https://github.com/remixz/umi" target="_blank" rel="noopener">View source on GitHub</a>.
</p>
</footer>

<div v-if="updateAvailable" class="fixed left-0 bottom-0 bg-yellow pa3 fw6 br1 shadow-1">
<span>An update is ready to be installed:</span>
<span class="f6 fw5 dib ml1 ba b--black bg-transparent bg-animate hover-bg-black hover-yellow br1 pointer ph2 pv1 tc" @click="refresh">Install and refresh</span>
</div>
<div :class="`fixed absolute--fill z-4 ${lights ? 'bg-black-90' : 'dn'}`"></div>
</div>
</template>
Expand Down Expand Up @@ -67,6 +70,9 @@ export default {
},
roomBarClass () {
return !this.connected ? 'hidden' : (this.hideBar ? 'peek' : 'show')
},
updateAvailable () {
return this.$store.state.updateAvailable
}
},
methods: {
Expand Down Expand Up @@ -97,6 +103,9 @@ export default {
this.$socket.off('change', this.wsOnChange)
this.$store.commit('UPDATE_CONNECTED', false)
this.$store.dispatch('leaveRoom')
},
refresh () {
location.reload()
}
},
watch: {
Expand All @@ -121,6 +130,14 @@ export default {
async beforeMount () {
await this.$store.dispatch('startSession')
this.loading = false
},
mounted () {
if (process.env.NODE_ENV === 'production') {
const runtime = require('offline-plugin/runtime')
this.$socket.on('app-update', () => {
runtime.update()
})
}
}
}
</script>
Expand Down
32 changes: 27 additions & 5 deletions src/components/Video.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,25 @@
</template>

<script>
/* global Clappr, LevelSelector */
/* global Clappr, LevelSelector, ClapprThumbnailsPlugin */
import $script from 'scriptjs'
import anime from 'animejs'
import api, {LOCALE, VERSION} from 'lib/api'
import emoji from 'lib/emoji'
import bif from 'lib/bif'
import Reactotron from './Reactotron'
export default {
name: 'video',
props: ['data', 'poster', 'id', 'seek', 'duration'],
props: ['data', 'poster', 'bif', 'id', 'seek', 'duration'],
components: {Reactotron},
data () {
return {
playerInit: false,
events: [],
lastEvent: null,
showBlur: true
showBlur: true,
frames: []
}
},
computed: {
Expand All @@ -44,7 +46,7 @@
}
},
mounted () {
$script('//cdn.jsdelivr.net/g/[email protected],[email protected]', () => {
$script('//cdn.jsdelivr.net/g/[email protected],[email protected],[email protected]', () => {
const self = this
this.playerInit = true
this.player = new Clappr.Player({
Expand All @@ -54,7 +56,7 @@
source: this.streamUrl,
poster: this.poster,
disableVideoTagContextMenu: true,
plugins: [LevelSelector],
plugins: [LevelSelector, ClapprThumbnailsPlugin],
levelSelectorConfig: {
title: 'Quality',
labels: {
Expand All @@ -65,6 +67,11 @@
0: '240p'
}
},
scrubThumbnails: {
backdropHeight: null,
spotlightHeight: 84,
thumbs: []
},
events: {
onReady () {
if (self.container) {
Expand Down Expand Up @@ -107,6 +114,10 @@
} else if (this.room !== '') {
this.wsRegisterEvents()
}
if (this.bif) {
this.loadBif()
}
})
},
watch: {
Expand All @@ -120,6 +131,7 @@
if (this.room !== '') {
this.playback.on(Clappr.Events.PLAYBACK_PLAY_INTENT, this.wsHandlePlay)
}
this.loadBif()
},
room (curr) {
if (curr === '') {
Expand Down Expand Up @@ -223,6 +235,16 @@
}
})
},
async loadBif () {
const thumbnailsPlugin = this.player.getPlugin('scrub-thumbnails')
if (this.frames.length > 0) {
thumbnailsPlugin.removeThumbnail(this.frames)
}
try {
this.frames = await bif(this.bif)
thumbnailsPlugin.addThumbnail(this.frames)
} catch (err) {}
},
wsJoinRoom () {
const time = parseInt(this.$route.query.wsTime, 10)
const playing = this.$route.query.wsPlaying
Expand Down
66 changes: 66 additions & 0 deletions src/lib/bif.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Based on https://github.com/chemoish/videojs-bif/blob/c6fdc0c2cfc9446927062995b7e8830ae45fff0d/src/parser.js
import axios from 'axios'
import { fromByteArray } from 'base64-js'
import { UMI_SERVER } from './api'

const BIF_INDEX_OFFSET = 64
const FRAMEWISE_SEPARATION_OFFSET = 16
const NUMBER_OF_BIF_IMAGES_OFFSET = 12
const BIF_INDEX_ENTRY_LENGTH = 8
const MAGIC_NUMBER = new Uint8Array([
'0x89',
'0x42',
'0x49',
'0x46',
'0x0d',
'0x0a',
'0x1a',
'0x0a'
])

function validate (magicNumber) {
let isValid = true

MAGIC_NUMBER.forEach((byte, i) => {
if (byte !== magicNumber[i]) {
isValid = false
}
})

return isValid
}

export default function bif (url) {
return new Promise(async (resolve, reject) => {
const {data: buf} = await axios.get(`${UMI_SERVER}/bif?bif=${url}`, {
responseType: 'arraybuffer'
})

const magicNumber = new Uint8Array(buf).slice(0, 8)
if (!validate(magicNumber)) {
return reject(new Error('Invalid BIF file'))
}

const data = new DataView(buf)
const framewiseSeparation = data.getUint32(FRAMEWISE_SEPARATION_OFFSET, true) || 1000
const numberOfBIFImages = data.getUint32(NUMBER_OF_BIF_IMAGES_OFFSET, true)

const bifData = []
for (let i = 0, bifIndexEntryOffset = BIF_INDEX_OFFSET; i < numberOfBIFImages; i += 1, bifIndexEntryOffset += BIF_INDEX_ENTRY_LENGTH) {
const bifIndexEntryTimestampOffset = bifIndexEntryOffset
const bifIndexEntryAbsoluteOffset = bifIndexEntryOffset + 4
const nextBifIndexEntryAbsoluteOffset = bifIndexEntryAbsoluteOffset + BIF_INDEX_ENTRY_LENGTH

const offset = data.getUint32(bifIndexEntryAbsoluteOffset, true)
const nextOffset = data.getUint32(nextBifIndexEntryAbsoluteOffset, true)
const length = nextOffset - offset

bifData.push({
time: ((data.getUint32(bifIndexEntryTimestampOffset, true) * framewiseSeparation) / 1000) - 15,
url: `data:image/jpeg;base64,${fromByteArray(new Uint8Array(buf.slice(offset, offset + length)))}`
})
}

resolve(bifData)
})
}
8 changes: 8 additions & 0 deletions src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,11 @@ new Vue({
store,
render: h => h(App)
})

if (process.env.NODE_ENV === 'production') {
const runtime = require('offline-plugin/runtime')
runtime.install({
onUpdateReady () { runtime.applyUpdate() },
onUpdated () { store.commit('SET_UPDATE_AVAILABLE') }
})
}
2 changes: 1 addition & 1 deletion src/pages/Media.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
<i class="fa fa-times pointer" aria-hidden="true" @click="nextEpisode = false"></i>
</div>
</div>
<umi-video v-if="streamData && streamData.format" :duration="media.duration" :data="streamData" :poster="media.screenshot_image.full_url" :id="$route.params.id" :seek="seek" @play="internalSeek = 0" @ended="playerEnded" />
<umi-video v-if="streamData && streamData.format" :duration="media.duration" :data="streamData" :poster="media.screenshot_image.full_url" :id="$route.params.id" :bif="media.bif_url" :seek="seek" @play="internalSeek = 0" @ended="playerEnded" />
<div v-else class="pv2">
<div class="bg-black absolute w-100 left-0 player-height player-top-offset">
<div class="bg-dark-gray center player-width player-height"></div>
Expand Down
9 changes: 7 additions & 2 deletions src/store/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import api, {ACCESS_TOKEN, DEVICE_TYPE, LOCALE, VERSION} from 'lib/api'
import {getUuid} from 'lib/auth'
import WS from 'lib/websocket'

const MEDIA_FIELDS = 'media.media_id,media.available,media.available_time,media.collection_id,media.series_id,media.type,media.episode_number,media.name,media.description,media.screenshot_image,media.created,media.duration,media.playhead'
const MEDIA_FIELDS = 'media.media_id,media.available,media.available_time,media.collection_id,media.series_id,media.type,media.episode_number,media.name,media.description,media.screenshot_image,media.created,media.duration,media.playhead,media.bif_url'
const SERIES_FIELDS = 'series.series_id,series.name,series.portrait_image,series.landscape_image,series.description,series.in_queue'

Vue.use(Vuex)
Expand All @@ -28,7 +28,8 @@ const store = new Vuex.Store({
roomId: '',
roomConnected: false,
connectedCount: 0,
lights: false
lights: false,
updateAvailable: false
},

actions: {
Expand Down Expand Up @@ -411,6 +412,10 @@ const store = new Vuex.Store({

UPDATE_LIGHTS (state, bool) {
state.lights = bool
},

SET_UPDATE_AVAILABLE (state) {
state.updateAvailable = true
}
}
})
Expand Down
Loading

0 comments on commit 87233cb

Please sign in to comment.