Client: handle multiple file resolutions
This commit is contained in:
parent
127d96b969
commit
aa8b6df4a5
20 changed files with 507 additions and 218 deletions
|
@ -8,7 +8,7 @@ const ExtractTextPlugin = require('extract-text-webpack-plugin')
|
|||
const PurifyCSSPlugin = require('purifycss-webpack')
|
||||
|
||||
module.exports = function (options) {
|
||||
const isProd = options.env === 'production'
|
||||
const isProd = options && options.env === 'production'
|
||||
|
||||
const configuration = {
|
||||
entry: {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { Video as VideoServerModel, VideoFile } from '../../../../../shared'
|
||||
import { User } from '../../shared'
|
||||
import { VideoResolution } from '../../../../../shared/models/videos/video-resolution.enum'
|
||||
|
||||
export class Video implements VideoServerModel {
|
||||
author: string
|
||||
|
@ -116,11 +117,19 @@ export class Video implements VideoServerModel {
|
|||
return (this.nsfw && (!user || user.displayNSFW === false))
|
||||
}
|
||||
|
||||
getDefaultMagnetUri () {
|
||||
getAppropriateMagnetUri (actualDownloadSpeed = 0) {
|
||||
if (this.files === undefined || this.files.length === 0) return ''
|
||||
if (this.files.length === 1) return this.files[0].magnetUri
|
||||
|
||||
// TODO: choose the original file
|
||||
return this.files[0].magnetUri
|
||||
// Find first video that is good for our download speed (remember they are sorted)
|
||||
let betterResolutionFile = this.files.find(f => actualDownloadSpeed > (f.size / this.duration))
|
||||
|
||||
// If the download speed is too bad, return the lowest resolution we have
|
||||
if (betterResolutionFile === undefined) {
|
||||
betterResolutionFile = this.files.find(f => f.resolution === VideoResolution.H_240P)
|
||||
}
|
||||
|
||||
return betterResolutionFile.magnetUri
|
||||
}
|
||||
|
||||
patch (values: Object) {
|
||||
|
|
|
@ -2,4 +2,3 @@ export * from './video-magnet.component'
|
|||
export * from './video-share.component'
|
||||
export * from './video-report.component'
|
||||
export * from './video-watch.component'
|
||||
export * from './webtorrent.service'
|
||||
|
|
|
@ -10,7 +10,10 @@
|
|||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<input #magnetUriInput (click)="magnetUriInput.select()" type="text" class="form-control input-sm readonly" readonly [value]="video.getDefaultMagnetUri()" />
|
||||
<div *ngFor="let file of video.files">
|
||||
<label>{{ file.resolutionLabel }}</label>
|
||||
<input #magnetUriInput (click)="magnetUriInput.select()" type="text" class="form-control input-sm readonly" readonly [value]="file.magnetUri" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -4,6 +4,8 @@ import { Observable } from 'rxjs/Observable'
|
|||
import { Subscription } from 'rxjs/Subscription'
|
||||
|
||||
import videojs from 'video.js'
|
||||
import '../../../assets/player/peertube-videojs-plugin'
|
||||
|
||||
import { MetaService } from '@ngx-meta/core'
|
||||
import { NotificationsService } from 'angular2-notifications'
|
||||
|
||||
|
@ -13,7 +15,7 @@ import { VideoShareComponent } from './video-share.component'
|
|||
import { VideoReportComponent } from './video-report.component'
|
||||
import { Video, VideoService } from '../shared'
|
||||
import { WebTorrentService } from './webtorrent.service'
|
||||
import { UserVideoRateType, VideoRateType, UserVideoRate } from '../../../../../shared'
|
||||
import { UserVideoRateType, VideoRateType } from '../../../../../shared'
|
||||
|
||||
@Component({
|
||||
selector: 'my-video-watch',
|
||||
|
@ -21,8 +23,6 @@ import { UserVideoRateType, VideoRateType, UserVideoRate } from '../../../../../
|
|||
styleUrls: [ './video-watch.component.scss' ]
|
||||
})
|
||||
export class VideoWatchComponent implements OnInit, OnDestroy {
|
||||
private static LOADTIME_TOO_LONG = 20000
|
||||
|
||||
@ViewChild('videoMagnetModal') videoMagnetModal: VideoMagnetComponent
|
||||
@ViewChild('videoShareModal') videoShareModal: VideoShareComponent
|
||||
@ViewChild('videoReportModal') videoReportModal: VideoReportComponent
|
||||
|
@ -38,20 +38,15 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
video: Video = null
|
||||
videoNotFound = false
|
||||
|
||||
private errorTimer: number
|
||||
private paramsSub: Subscription
|
||||
private errorsSub: Subscription
|
||||
private torrentInfosInterval: number
|
||||
|
||||
constructor (
|
||||
private elementRef: ElementRef,
|
||||
private ngZone: NgZone,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private videoService: VideoService,
|
||||
private confirmService: ConfirmService,
|
||||
private metaService: MetaService,
|
||||
private webTorrentService: WebTorrentService,
|
||||
private authService: AuthService,
|
||||
private notificationsService: NotificationsService
|
||||
) {}
|
||||
|
@ -68,81 +63,17 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
)
|
||||
})
|
||||
|
||||
this.playerElement = this.elementRef.nativeElement.querySelector('#video-container')
|
||||
|
||||
const videojsOptions = {
|
||||
controls: true,
|
||||
autoplay: true
|
||||
}
|
||||
|
||||
const self = this
|
||||
videojs(this.playerElement, videojsOptions, function () {
|
||||
self.player = this
|
||||
})
|
||||
|
||||
this.errorsSub = this.webTorrentService.errors.subscribe(err => this.handleError(err))
|
||||
}
|
||||
|
||||
ngOnDestroy () {
|
||||
// Remove WebTorrent stuff
|
||||
console.log('Removing video from webtorrent.')
|
||||
window.clearInterval(this.torrentInfosInterval)
|
||||
window.clearTimeout(this.errorTimer)
|
||||
|
||||
if (this.video !== null && this.webTorrentService.has(this.video.getDefaultMagnetUri())) {
|
||||
this.webTorrentService.remove(this.video.getDefaultMagnetUri())
|
||||
}
|
||||
|
||||
// Remove player
|
||||
videojs(this.playerElement).dispose()
|
||||
|
||||
// Unsubscribe subscriptions
|
||||
this.paramsSub.unsubscribe()
|
||||
this.errorsSub.unsubscribe()
|
||||
}
|
||||
|
||||
loadVideo () {
|
||||
// Reset the error
|
||||
this.error = false
|
||||
// We are loading the video
|
||||
this.loading = true
|
||||
|
||||
console.log('Adding ' + this.video.getDefaultMagnetUri() + '.')
|
||||
|
||||
// The callback might never return if there are network issues
|
||||
// So we create a timer to inform the user the load is abnormally long
|
||||
this.errorTimer = window.setTimeout(() => this.loadTooLong(), VideoWatchComponent.LOADTIME_TOO_LONG)
|
||||
|
||||
const torrent = this.webTorrentService.add(this.video.getDefaultMagnetUri(), torrent => {
|
||||
// Clear the error timer
|
||||
window.clearTimeout(this.errorTimer)
|
||||
// Maybe the error was fired by the timer, so reset it
|
||||
this.error = false
|
||||
|
||||
// We are not loading the video anymore
|
||||
this.loading = false
|
||||
|
||||
console.log('Added ' + this.video.getDefaultMagnetUri() + '.')
|
||||
torrent.files[0].renderTo(this.playerElement, (err) => {
|
||||
if (err) {
|
||||
this.notificationsService.error('Error', 'Cannot append the file in the video element.')
|
||||
console.error(err)
|
||||
}
|
||||
|
||||
// Hack to "simulate" src link in video.js >= 6
|
||||
// If no, we can't play the video after pausing it
|
||||
// https://github.com/videojs/video.js/blob/master/src/js/player.js#L1633
|
||||
(this.player as any).src = () => true
|
||||
|
||||
this.player.play()
|
||||
})
|
||||
|
||||
this.runInProgress(torrent)
|
||||
})
|
||||
|
||||
torrent.on('error', err => this.handleError(err))
|
||||
torrent.on('warning', err => this.handleError(err))
|
||||
}
|
||||
|
||||
setLike () {
|
||||
|
@ -295,8 +226,36 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
return this.router.navigate([ '/videos/list' ])
|
||||
}
|
||||
|
||||
this.playerElement = this.elementRef.nativeElement.querySelector('#video-container')
|
||||
|
||||
const videojsOptions = {
|
||||
controls: true,
|
||||
autoplay: true,
|
||||
plugins: {
|
||||
peertube: {
|
||||
videoFiles: this.video.files,
|
||||
playerElement: this.playerElement,
|
||||
autoplay: true,
|
||||
peerTubeLink: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const self = this
|
||||
videojs(this.playerElement, videojsOptions, function () {
|
||||
self.player = this
|
||||
this.on('customError', (event, data) => {
|
||||
self.handleError(data.err)
|
||||
})
|
||||
|
||||
this.on('torrentInfo', (event, data) => {
|
||||
self.downloadSpeed = data.downloadSpeed
|
||||
self.numPeers = data.numPeers
|
||||
self.uploadSpeed = data.uploadSpeed
|
||||
})
|
||||
})
|
||||
|
||||
this.setOpenGraphTags()
|
||||
this.loadVideo()
|
||||
this.checkUserRating()
|
||||
}
|
||||
)
|
||||
|
@ -318,11 +277,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
this.video.dislikes += dislikesToIncrement
|
||||
}
|
||||
|
||||
private loadTooLong () {
|
||||
this.error = true
|
||||
console.error('The video load seems to be abnormally long.')
|
||||
}
|
||||
|
||||
private setOpenGraphTags () {
|
||||
this.metaService.setTitle(this.video.name)
|
||||
|
||||
|
@ -343,15 +297,4 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
this.metaService.setTag('og:url', window.location.href)
|
||||
this.metaService.setTag('url', window.location.href)
|
||||
}
|
||||
|
||||
private runInProgress (torrent: any) {
|
||||
// Refresh each second
|
||||
this.torrentInfosInterval = window.setInterval(() => {
|
||||
this.ngZone.run(() => {
|
||||
this.downloadSpeed = torrent.downloadSpeed
|
||||
this.numPeers = torrent.numPeers
|
||||
this.uploadSpeed = torrent.uploadSpeed
|
||||
})
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,29 +0,0 @@
|
|||
import { Injectable } from '@angular/core'
|
||||
import { Subject } from 'rxjs/Subject'
|
||||
|
||||
import * as WebTorrent from 'webtorrent'
|
||||
|
||||
@Injectable()
|
||||
export class WebTorrentService {
|
||||
errors = new Subject<string | Error>()
|
||||
|
||||
private client: WebTorrent.Instance
|
||||
|
||||
constructor () {
|
||||
this.client = new WebTorrent({ dht: false })
|
||||
|
||||
this.client.on('error', err => this.errors.next(err))
|
||||
}
|
||||
|
||||
add (magnetUri: string, callback: (torrent: WebTorrent.Torrent) => any) {
|
||||
return this.client.add(magnetUri, callback)
|
||||
}
|
||||
|
||||
remove (magnetUri: string) {
|
||||
return this.client.remove(magnetUri)
|
||||
}
|
||||
|
||||
has (magnetUri: string) {
|
||||
return this.client.get(magnetUri) !== null
|
||||
}
|
||||
}
|
|
@ -10,8 +10,7 @@ import {
|
|||
VideoWatchComponent,
|
||||
VideoMagnetComponent,
|
||||
VideoReportComponent,
|
||||
VideoShareComponent,
|
||||
WebTorrentService
|
||||
VideoShareComponent
|
||||
} from './video-watch'
|
||||
import { VideoService } from './shared'
|
||||
import { SharedModule } from '../shared'
|
||||
|
@ -47,8 +46,7 @@ import { SharedModule } from '../shared'
|
|||
],
|
||||
|
||||
providers: [
|
||||
VideoService,
|
||||
WebTorrentService
|
||||
VideoService
|
||||
]
|
||||
})
|
||||
export class VideosModule { }
|
||||
|
|
238
client/src/assets/player/peertube-videojs-plugin.ts
Normal file
238
client/src/assets/player/peertube-videojs-plugin.ts
Normal file
|
@ -0,0 +1,238 @@
|
|||
// Big thanks to: https://github.com/kmoskwiak/videojs-resolution-switcher
|
||||
|
||||
import videojs, { Player } from 'video.js'
|
||||
import * as WebTorrent from 'webtorrent'
|
||||
|
||||
import { renderVideo } from './video-renderer'
|
||||
import { VideoFile } from '../../../../shared'
|
||||
|
||||
// videojs typings don't have some method we need
|
||||
const videojsUntyped = videojs as any
|
||||
const webtorrent = new WebTorrent({ dht: false })
|
||||
|
||||
const MenuItem = videojsUntyped.getComponent('MenuItem')
|
||||
const ResolutionMenuItem = videojsUntyped.extend(MenuItem, {
|
||||
constructor: function (player: Player, options) {
|
||||
options.selectable = true
|
||||
MenuItem.call(this, player, options)
|
||||
|
||||
const currentResolution = this.player_.getCurrentResolution()
|
||||
this.selected(this.options_.id === currentResolution)
|
||||
},
|
||||
|
||||
handleClick: function (event) {
|
||||
MenuItem.prototype.handleClick.call(this, event)
|
||||
this.player_.updateResolution(this.options_.id)
|
||||
}
|
||||
})
|
||||
MenuItem.registerComponent('ResolutionMenuItem', ResolutionMenuItem)
|
||||
|
||||
const MenuButton = videojsUntyped.getComponent('MenuButton')
|
||||
const ResolutionMenuButton = videojsUntyped.extend(MenuButton, {
|
||||
constructor: function (player, options) {
|
||||
this.label = document.createElement('span')
|
||||
options.label = 'Quality'
|
||||
|
||||
MenuButton.call(this, player, options)
|
||||
this.el().setAttribute('aria-label', 'Quality')
|
||||
this.controlText('Quality')
|
||||
|
||||
videojsUntyped.dom.addClass(this.label, 'vjs-resolution-button-label')
|
||||
this.el().appendChild(this.label)
|
||||
|
||||
player.on('videoFileUpdate', videojs.bind(this, this.update))
|
||||
},
|
||||
|
||||
createItems: function () {
|
||||
const menuItems = []
|
||||
for (const videoFile of this.player_.videoFiles) {
|
||||
menuItems.push(new ResolutionMenuItem(
|
||||
this.player_,
|
||||
{
|
||||
id: videoFile.resolution,
|
||||
label: videoFile.resolutionLabel,
|
||||
src: videoFile.magnetUri,
|
||||
selected: videoFile.resolution === this.currentSelection
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
return menuItems
|
||||
},
|
||||
|
||||
update: function () {
|
||||
this.label.innerHTML = this.player_.getCurrentResolutionLabel()
|
||||
return MenuButton.prototype.update.call(this)
|
||||
},
|
||||
|
||||
buildCSSClass: function () {
|
||||
return MenuButton.prototype.buildCSSClass.call(this) + ' vjs-resolution-button'
|
||||
}
|
||||
})
|
||||
MenuButton.registerComponent('ResolutionMenuButton', ResolutionMenuButton)
|
||||
|
||||
const Button = videojsUntyped.getComponent('Button')
|
||||
const PeertubeLinkButton = videojsUntyped.extend(Button, {
|
||||
constructor: function (player) {
|
||||
Button.apply(this, arguments)
|
||||
this.player = player
|
||||
},
|
||||
|
||||
createEl: function () {
|
||||
const link = document.createElement('a')
|
||||
link.href = window.location.href.replace('embed', 'watch')
|
||||
link.innerHTML = 'PeerTube'
|
||||
link.title = 'Go to the video page'
|
||||
link.className = 'vjs-peertube-link'
|
||||
link.target = '_blank'
|
||||
|
||||
return link
|
||||
},
|
||||
|
||||
handleClick: function () {
|
||||
this.player.pause()
|
||||
}
|
||||
})
|
||||
Button.registerComponent('PeerTubeLinkButton', PeertubeLinkButton)
|
||||
|
||||
type PeertubePluginOptions = {
|
||||
videoFiles: VideoFile[]
|
||||
playerElement: HTMLVideoElement
|
||||
autoplay: boolean
|
||||
peerTubeLink: boolean
|
||||
}
|
||||
const peertubePlugin = function (options: PeertubePluginOptions) {
|
||||
const player = this
|
||||
let currentVideoFile: VideoFile = undefined
|
||||
const playerElement = options.playerElement
|
||||
player.videoFiles = options.videoFiles
|
||||
|
||||
// Hack to "simulate" src link in video.js >= 6
|
||||
// Without this, we can't play the video after pausing it
|
||||
// https://github.com/videojs/video.js/blob/master/src/js/player.js#L1633
|
||||
player.src = function () {
|
||||
return true
|
||||
}
|
||||
|
||||
player.getCurrentResolution = function () {
|
||||
return currentVideoFile ? currentVideoFile.resolution : -1
|
||||
}
|
||||
|
||||
player.getCurrentResolutionLabel = function () {
|
||||
return currentVideoFile ? currentVideoFile.resolutionLabel : ''
|
||||
}
|
||||
|
||||
player.updateVideoFile = function (videoFile: VideoFile, done: () => void) {
|
||||
if (done === undefined) {
|
||||
done = () => { /* empty */ }
|
||||
}
|
||||
|
||||
// Pick the first one
|
||||
if (videoFile === undefined) {
|
||||
videoFile = player.videoFiles[0]
|
||||
}
|
||||
|
||||
// Don't add the same video file once again
|
||||
if (currentVideoFile !== undefined && currentVideoFile.magnetUri === videoFile.magnetUri) {
|
||||
return
|
||||
}
|
||||
|
||||
const previousVideoFile = currentVideoFile
|
||||
currentVideoFile = videoFile
|
||||
|
||||
console.log('Adding ' + videoFile.magnetUri + '.')
|
||||
player.torrent = webtorrent.add(videoFile.magnetUri, torrent => {
|
||||
console.log('Added ' + videoFile.magnetUri + '.')
|
||||
|
||||
this.flushVideoFile(previousVideoFile)
|
||||
|
||||
const options = { autoplay: true, controls: true }
|
||||
renderVideo(torrent.files[0], playerElement, options,(err, renderer) => {
|
||||
if (err) return handleError(err)
|
||||
|
||||
this.renderer = renderer
|
||||
player.play()
|
||||
|
||||
return done()
|
||||
})
|
||||
})
|
||||
|
||||
player.torrent.on('error', err => handleError(err))
|
||||
player.torrent.on('warning', err => handleError(err))
|
||||
|
||||
player.trigger('videoFileUpdate')
|
||||
|
||||
return player
|
||||
}
|
||||
|
||||
player.updateResolution = function (resolution) {
|
||||
// Remember player state
|
||||
const currentTime = player.currentTime()
|
||||
const isPaused = player.paused()
|
||||
|
||||
// Hide bigPlayButton
|
||||
if (!isPaused && this.player_.options_.bigPlayButton) {
|
||||
this.player_.bigPlayButton.hide()
|
||||
}
|
||||
|
||||
const newVideoFile = player.videoFiles.find(f => f.resolution === resolution)
|
||||
player.updateVideoFile(newVideoFile, () => {
|
||||
player.currentTime(currentTime)
|
||||
player.handleTechSeeked_()
|
||||
})
|
||||
}
|
||||
|
||||
player.flushVideoFile = function (videoFile: VideoFile, destroyRenderer = true) {
|
||||
if (videoFile !== undefined && webtorrent.get(videoFile.magnetUri)) {
|
||||
if (destroyRenderer === true) this.renderer.destroy()
|
||||
webtorrent.remove(videoFile.magnetUri)
|
||||
}
|
||||
}
|
||||
|
||||
player.ready(function () {
|
||||
const controlBar = player.controlBar
|
||||
|
||||
const menuButton = new ResolutionMenuButton(player, options)
|
||||
const fullscreenElement = controlBar.fullscreenToggle.el()
|
||||
controlBar.resolutionSwitcher = controlBar.el().insertBefore(menuButton.el(), fullscreenElement)
|
||||
controlBar.resolutionSwitcher.dispose = function () {
|
||||
this.parentNode.removeChild(this)
|
||||
}
|
||||
|
||||
player.dispose = function () {
|
||||
// Don't need to destroy renderer, video player will be destroyed
|
||||
player.flushVideoFile(currentVideoFile, false)
|
||||
}
|
||||
|
||||
if (options.peerTubeLink === true) {
|
||||
const peerTubeLinkButton = new PeertubeLinkButton(player)
|
||||
controlBar.peerTubeLink = controlBar.el().insertBefore(peerTubeLinkButton.el(), fullscreenElement)
|
||||
|
||||
controlBar.peerTubeLink.dispose = function () {
|
||||
this.parentNode.removeChild(this)
|
||||
}
|
||||
}
|
||||
|
||||
if (options.autoplay === true) {
|
||||
player.updateVideoFile()
|
||||
} else {
|
||||
player.one('play', () => player.updateVideoFile())
|
||||
}
|
||||
|
||||
setInterval(() => {
|
||||
if (player.torrent !== undefined) {
|
||||
player.trigger('torrentInfo', {
|
||||
downloadSpeed: player.torrent.downloadSpeed,
|
||||
numPeers: player.torrent.numPeers,
|
||||
uploadSpeed: player.torrent.uploadSpeed
|
||||
})
|
||||
}
|
||||
}, 1000)
|
||||
})
|
||||
|
||||
function handleError (err: Error|string) {
|
||||
return player.trigger('customError', { err })
|
||||
}
|
||||
}
|
||||
|
||||
videojsUntyped.registerPlugin('peertube', peertubePlugin)
|
119
client/src/assets/player/video-renderer.ts
Normal file
119
client/src/assets/player/video-renderer.ts
Normal file
|
@ -0,0 +1,119 @@
|
|||
// Thanks: https://github.com/feross/render-media
|
||||
// TODO: use render-media once https://github.com/feross/render-media/issues/32 is fixed
|
||||
|
||||
import { extname } from 'path'
|
||||
import * as MediaElementWrapper from 'mediasource'
|
||||
import * as videostream from 'videostream'
|
||||
|
||||
const VIDEOSTREAM_EXTS = [
|
||||
'.m4a',
|
||||
'.m4v',
|
||||
'.mp4'
|
||||
]
|
||||
|
||||
type RenderMediaOptions = {
|
||||
controls: boolean
|
||||
autoplay: boolean
|
||||
}
|
||||
|
||||
function renderVideo (
|
||||
file,
|
||||
elem: HTMLVideoElement,
|
||||
opts: RenderMediaOptions,
|
||||
callback: (err: Error, renderer: any) => void
|
||||
) {
|
||||
validateFile(file)
|
||||
|
||||
return renderMedia(file, elem, opts, callback)
|
||||
}
|
||||
|
||||
function renderMedia (file, elem: HTMLVideoElement, opts: RenderMediaOptions, callback: (err: Error, renderer: any) => void) {
|
||||
const extension = extname(file.name).toLowerCase()
|
||||
let preparedElem = undefined
|
||||
let currentTime = 0
|
||||
let renderer
|
||||
|
||||
if (VIDEOSTREAM_EXTS.indexOf(extension) >= 0) {
|
||||
renderer = useVideostream()
|
||||
} else {
|
||||
renderer = useMediaSource()
|
||||
}
|
||||
|
||||
function useVideostream () {
|
||||
prepareElem()
|
||||
preparedElem.addEventListener('error', fallbackToMediaSource)
|
||||
preparedElem.addEventListener('loadstart', onLoadStart)
|
||||
preparedElem.addEventListener('canplay', onCanPlay)
|
||||
return videostream(file, preparedElem)
|
||||
}
|
||||
|
||||
function useMediaSource () {
|
||||
prepareElem()
|
||||
preparedElem.addEventListener('error', callback)
|
||||
preparedElem.addEventListener('loadstart', onLoadStart)
|
||||
preparedElem.addEventListener('canplay', onCanPlay)
|
||||
|
||||
const wrapper = new MediaElementWrapper(preparedElem)
|
||||
const writable = wrapper.createWriteStream(getCodec(file.name))
|
||||
file.createReadStream().pipe(writable)
|
||||
|
||||
if (currentTime) preparedElem.currentTime = currentTime
|
||||
|
||||
return wrapper
|
||||
}
|
||||
|
||||
function fallbackToMediaSource () {
|
||||
preparedElem.removeEventListener('error', fallbackToMediaSource)
|
||||
preparedElem.removeEventListener('canplay', onCanPlay)
|
||||
|
||||
useMediaSource()
|
||||
}
|
||||
|
||||
function prepareElem () {
|
||||
if (preparedElem === undefined) {
|
||||
preparedElem = elem
|
||||
|
||||
preparedElem.addEventListener('progress', function () {
|
||||
currentTime = elem.currentTime
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function onLoadStart () {
|
||||
preparedElem.removeEventListener('loadstart', onLoadStart)
|
||||
if (opts.autoplay) preparedElem.play()
|
||||
}
|
||||
|
||||
function onCanPlay () {
|
||||
preparedElem.removeEventListener('canplay', onCanPlay)
|
||||
callback(null, renderer)
|
||||
}
|
||||
}
|
||||
|
||||
function validateFile (file) {
|
||||
if (file == null) {
|
||||
throw new Error('file cannot be null or undefined')
|
||||
}
|
||||
if (typeof file.name !== 'string') {
|
||||
throw new Error('missing or invalid file.name property')
|
||||
}
|
||||
if (typeof file.createReadStream !== 'function') {
|
||||
throw new Error('missing or invalid file.createReadStream property')
|
||||
}
|
||||
}
|
||||
|
||||
function getCodec (name: string) {
|
||||
const ext = extname(name).toLowerCase()
|
||||
return {
|
||||
'.m4a': 'audio/mp4; codecs="mp4a.40.5"',
|
||||
'.m4v': 'video/mp4; codecs="avc1.640029, mp4a.40.5"',
|
||||
'.mkv': 'video/webm; codecs="avc1.640029, mp4a.40.5"',
|
||||
'.mp3': 'audio/mpeg',
|
||||
'.mp4': 'video/mp4; codecs="avc1.640029, mp4a.40.5"',
|
||||
'.webm': 'video/webm; codecs="vorbis, vp8"'
|
||||
}[ext]
|
||||
}
|
||||
|
||||
export {
|
||||
renderVideo
|
||||
}
|
|
@ -1,3 +1,33 @@
|
|||
// Thanks: https://github.com/kmoskwiak/videojs-resolution-switcher/pull/92/files
|
||||
.vjs-resolution-button-label {
|
||||
font-size: 1em;
|
||||
line-height: 3em;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -1px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
.vjs-resolution-button {
|
||||
outline: 0 !important;
|
||||
|
||||
.vjs-menu {
|
||||
.vjs-menu-content {
|
||||
width: 4em;
|
||||
left: 50%; /* Center the menu, in it's parent */
|
||||
margin-left: -2em; /* half of width, to center */
|
||||
}
|
||||
|
||||
li {
|
||||
text-transform: none;
|
||||
font-size: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Thanks: https://github.com/zanechua/videojs-sublime-inspired-skin
|
||||
|
||||
// Video JS Sublime Skin
|
||||
|
@ -210,7 +240,7 @@ $slider-bg-color: lighten($primary-background-color, 33%);
|
|||
width: 6em;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
margin-right: 30px;
|
||||
margin-right: 65px;
|
||||
}
|
||||
|
||||
.vjs-sublime-skin .vjs-volume-menu-button .vjs-menu-content,
|
||||
|
|
|
@ -29,7 +29,11 @@ html, body {
|
|||
line-height: 2.20;
|
||||
transition: all .4s;
|
||||
position: relative;
|
||||
right: 6px;
|
||||
right: 8px;
|
||||
}
|
||||
|
||||
.vjs-resolution-button-label {
|
||||
left: -7px;
|
||||
}
|
||||
|
||||
.vjs-peertube-link:hover {
|
||||
|
@ -38,5 +42,5 @@ html, body {
|
|||
|
||||
// Fix volume panel because we added a new component (PeerTube link)
|
||||
.vjs-volume-panel {
|
||||
margin-right: 90px !important;
|
||||
margin-right: 130px !important;
|
||||
}
|
||||
|
|
|
@ -1,14 +1,11 @@
|
|||
import './embed.scss'
|
||||
|
||||
import videojs from 'video.js'
|
||||
import '../../assets/player/peertube-videojs-plugin'
|
||||
import 'videojs-dock/dist/videojs-dock.es.js'
|
||||
import * as WebTorrent from 'webtorrent'
|
||||
import { Video } from '../../../../shared'
|
||||
|
||||
// videojs typings don't have some method we need
|
||||
const videojsUntyped = videojs as any
|
||||
|
||||
function loadVideoInfos (videoId: string, callback: (err: Error, res?: Video) => void) {
|
||||
function loadVideoInfo (videoId: string, callback: (err: Error, res?: Video) => void) {
|
||||
const xhttp = new XMLHttpRequest()
|
||||
xhttp.onreadystatechange = function () {
|
||||
if (this.readyState === 4 && this.status === 200) {
|
||||
|
@ -24,84 +21,36 @@ function loadVideoInfos (videoId: string, callback: (err: Error, res?: Video) =>
|
|||
xhttp.send()
|
||||
}
|
||||
|
||||
function loadVideoTorrent (magnetUri: string, player: videojs.Player) {
|
||||
console.log('Loading video ' + videoId)
|
||||
const client = new WebTorrent()
|
||||
|
||||
console.log('Adding magnet ' + magnetUri)
|
||||
client.add(magnetUri, torrent => {
|
||||
const file = torrent.files[0]
|
||||
|
||||
file.renderTo('video', err => {
|
||||
if (err) {
|
||||
console.error(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Hack to "simulate" src link in video.js >= 6
|
||||
// If no, we can't play the video after pausing it
|
||||
// https://github.com/videojs/video.js/blob/master/src/js/player.js#L1633
|
||||
(player as any).src = () => true
|
||||
|
||||
player.play()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const urlParts = window.location.href.split('/')
|
||||
const videoId = urlParts[urlParts.length - 1]
|
||||
|
||||
loadVideoInfos(videoId, (err, videoInfos) => {
|
||||
loadVideoInfo(videoId, (err, videoInfo) => {
|
||||
if (err) {
|
||||
console.error(err)
|
||||
return
|
||||
}
|
||||
|
||||
let magnetUri = ''
|
||||
if (videoInfos.files !== undefined && videoInfos.files.length !== 0) {
|
||||
magnetUri = videoInfos.files[0].magnetUri
|
||||
const videoElement = document.getElementById('video-container') as HTMLVideoElement
|
||||
const previewUrl = window.location.origin + videoInfo.previewPath
|
||||
videoElement.poster = previewUrl
|
||||
|
||||
const videojsOptions = {
|
||||
controls: true,
|
||||
autoplay: false,
|
||||
plugins: {
|
||||
peertube: {
|
||||
videoFiles: videoInfo.files,
|
||||
playerElement: videoElement,
|
||||
autoplay: false,
|
||||
peerTubeLink: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const videoContainer = document.getElementById('video-container') as HTMLVideoElement
|
||||
const previewUrl = window.location.origin + videoInfos.previewPath
|
||||
videoContainer.poster = previewUrl
|
||||
|
||||
videojs('video-container', { controls: true, autoplay: false }, function () {
|
||||
videojs('video-container', videojsOptions, function () {
|
||||
const player = this
|
||||
|
||||
const Button = videojsUntyped.getComponent('Button')
|
||||
const peertubeLinkButton = videojsUntyped.extend(Button, {
|
||||
constructor: function () {
|
||||
Button.apply(this, arguments)
|
||||
},
|
||||
|
||||
createEl: function () {
|
||||
const link = document.createElement('a')
|
||||
link.href = window.location.href.replace('embed', 'watch')
|
||||
link.innerHTML = 'PeerTube'
|
||||
link.title = 'Go to the video page'
|
||||
link.className = 'vjs-peertube-link'
|
||||
link.target = '_blank'
|
||||
|
||||
return link
|
||||
},
|
||||
|
||||
handleClick: function () {
|
||||
player.pause()
|
||||
}
|
||||
})
|
||||
videojsUntyped.registerComponent('PeerTubeLinkButton', peertubeLinkButton)
|
||||
|
||||
const controlBar = player.getChild('controlBar')
|
||||
const addedLink = controlBar.addChild('PeerTubeLinkButton', {})
|
||||
controlBar.el().insertBefore(addedLink.el(), controlBar.fullscreenToggle.el())
|
||||
|
||||
player.dock({
|
||||
title: videoInfos.name
|
||||
title: videoInfo.name
|
||||
})
|
||||
|
||||
document.querySelector('.vjs-big-play-button').addEventListener('click', () => {
|
||||
loadVideoTorrent(magnetUri, player)
|
||||
}, false)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -26,3 +26,6 @@ user:
|
|||
|
||||
signup:
|
||||
limit: 4
|
||||
|
||||
transcoding:
|
||||
enabled: false
|
||||
|
|
|
@ -20,3 +20,6 @@ storage:
|
|||
|
||||
admin:
|
||||
email: 'admin3@example.com'
|
||||
|
||||
transcoding:
|
||||
enabled: false
|
||||
|
|
|
@ -20,3 +20,6 @@ storage:
|
|||
|
||||
admin:
|
||||
email: 'admin4@example.com'
|
||||
|
||||
transcoding:
|
||||
enabled: false
|
||||
|
|
|
@ -20,3 +20,6 @@ storage:
|
|||
|
||||
admin:
|
||||
email: 'admin5@example.com'
|
||||
|
||||
transcoding:
|
||||
enabled: false
|
||||
|
|
|
@ -20,3 +20,6 @@ storage:
|
|||
|
||||
admin:
|
||||
email: 'admin6@example.com'
|
||||
|
||||
transcoding:
|
||||
enabled: false
|
||||
|
|
|
@ -10,3 +10,7 @@ database:
|
|||
|
||||
signup:
|
||||
enabled: true
|
||||
|
||||
transcoding:
|
||||
enabled: true
|
||||
threads: 4
|
||||
|
|
|
@ -477,19 +477,26 @@ toFormattedJSON = function (this: VideoInstance) {
|
|||
files: []
|
||||
}
|
||||
|
||||
this.VideoFiles.forEach(videoFile => {
|
||||
let resolutionLabel = VIDEO_FILE_RESOLUTIONS[videoFile.resolution]
|
||||
if (!resolutionLabel) resolutionLabel = 'Unknown'
|
||||
// Format and sort video files
|
||||
json.files = this.VideoFiles
|
||||
.map(videoFile => {
|
||||
let resolutionLabel = VIDEO_FILE_RESOLUTIONS[videoFile.resolution]
|
||||
if (!resolutionLabel) resolutionLabel = 'Unknown'
|
||||
|
||||
const videoFileJson = {
|
||||
resolution: videoFile.resolution,
|
||||
resolutionLabel,
|
||||
magnetUri: this.generateMagnetUri(videoFile),
|
||||
size: videoFile.size
|
||||
}
|
||||
const videoFileJson = {
|
||||
resolution: videoFile.resolution,
|
||||
resolutionLabel,
|
||||
magnetUri: this.generateMagnetUri(videoFile),
|
||||
size: videoFile.size
|
||||
}
|
||||
|
||||
json.files.push(videoFileJson)
|
||||
})
|
||||
return videoFileJson
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (a.resolution < b.resolution) return 1
|
||||
if (a.resolution === b.resolution) return 0
|
||||
return -1
|
||||
})
|
||||
|
||||
return json
|
||||
}
|
||||
|
|
|
@ -195,27 +195,27 @@ describe('Test multiple pods', function () {
|
|||
const originalFile = video.files.find(f => f.resolution === 0)
|
||||
expect(originalFile).not.to.be.undefined
|
||||
expect(originalFile.resolutionLabel).to.equal('original')
|
||||
expect(originalFile.size).to.equal(711327)
|
||||
expect(originalFile.size).to.be.above(700000).and.below(720000)
|
||||
|
||||
const file240p = video.files.find(f => f.resolution === 240)
|
||||
expect(file240p).not.to.be.undefined
|
||||
expect(file240p.resolutionLabel).to.equal('240p')
|
||||
expect(file240p.size).to.equal(139953)
|
||||
expect(file240p.size).to.be.above(130000).and.below(150000)
|
||||
|
||||
const file360p = video.files.find(f => f.resolution === 360)
|
||||
expect(file360p).not.to.be.undefined
|
||||
expect(file360p.resolutionLabel).to.equal('360p')
|
||||
expect(file360p.size).to.equal(169926)
|
||||
expect(file360p.size).to.be.above(160000).and.below(180000)
|
||||
|
||||
const file480p = video.files.find(f => f.resolution === 480)
|
||||
expect(file480p).not.to.be.undefined
|
||||
expect(file480p.resolutionLabel).to.equal('480p')
|
||||
expect(file480p.size).to.equal(206758)
|
||||
expect(file480p.size).to.be.above(200000).and.below(220000)
|
||||
|
||||
const file720p = video.files.find(f => f.resolution === 720)
|
||||
expect(file720p).not.to.be.undefined
|
||||
expect(file720p.resolutionLabel).to.equal('720p')
|
||||
expect(file720p.size).to.equal(314913)
|
||||
expect(file720p.size).to.be.above(310000).and.below(320000)
|
||||
|
||||
const test = await testVideoImage(server.url, 'video_short2.webm', video.thumbnailPath)
|
||||
expect(test).to.equal(true)
|
||||
|
|
Loading…
Reference in a new issue