mirror of https://github.com/Chocobozzz/PeerTube
Merge branch 'release/6.3.0' into develop
commit
47a7007f74
13
CHANGELOG.md
13
CHANGELOG.md
|
@ -1,5 +1,18 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v6.3.2
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
|
||||||
|
* Fix 403 error when downloading private/internal video
|
||||||
|
* Don't crash video federation and live replay generation on missing thumbnail/preview
|
||||||
|
* Fix advanced search input with multiple automatic search tokens
|
||||||
|
* Fix player "Copy URL" when the video is fullscreen
|
||||||
|
* Fix account videos search
|
||||||
|
* Add missing max transcoding fps config in admin
|
||||||
|
* Don't add mobile buttons if the player controls are disabled
|
||||||
|
|
||||||
|
|
||||||
## v6.3.1
|
## v6.3.1
|
||||||
|
|
||||||
### IMPORTANT NOTES
|
### IMPORTANT NOTES
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "peertube-client",
|
"name": "peertube-client",
|
||||||
"version": "6.3.1",
|
"version": "6.3.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"author": {
|
"author": {
|
||||||
|
|
|
@ -22,6 +22,7 @@ import {
|
||||||
SERVICES_TWITTER_USERNAME_VALIDATOR,
|
SERVICES_TWITTER_USERNAME_VALIDATOR,
|
||||||
SIGNUP_LIMIT_VALIDATOR,
|
SIGNUP_LIMIT_VALIDATOR,
|
||||||
SIGNUP_MINIMUM_AGE_VALIDATOR,
|
SIGNUP_MINIMUM_AGE_VALIDATOR,
|
||||||
|
TRANSCODING_MAX_FPS_VALIDATOR,
|
||||||
TRANSCODING_THREADS_VALIDATOR
|
TRANSCODING_THREADS_VALIDATOR
|
||||||
} from '@app/shared/form-validators/custom-config-validators'
|
} from '@app/shared/form-validators/custom-config-validators'
|
||||||
import { USER_VIDEO_QUOTA_DAILY_VALIDATOR, USER_VIDEO_QUOTA_VALIDATOR } from '@app/shared/form-validators/user-validators'
|
import { USER_VIDEO_QUOTA_DAILY_VALIDATOR, USER_VIDEO_QUOTA_VALIDATOR } from '@app/shared/form-validators/user-validators'
|
||||||
|
@ -241,6 +242,9 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
||||||
},
|
},
|
||||||
remoteRunners: {
|
remoteRunners: {
|
||||||
enabled: null
|
enabled: null
|
||||||
|
},
|
||||||
|
fps: {
|
||||||
|
max: TRANSCODING_MAX_FPS_VALIDATOR
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
live: {
|
live: {
|
||||||
|
@ -262,6 +266,9 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
||||||
alwaysTranscodeOriginalResolution: null,
|
alwaysTranscodeOriginalResolution: null,
|
||||||
remoteRunners: {
|
remoteRunners: {
|
||||||
enabled: null
|
enabled: null
|
||||||
|
},
|
||||||
|
fps: {
|
||||||
|
max: TRANSCODING_MAX_FPS_VALIDATOR
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -48,7 +48,7 @@
|
||||||
<div class="form-group" [ngClass]="getDisabledLiveClass()">
|
<div class="form-group" [ngClass]="getDisabledLiveClass()">
|
||||||
<label i18n for="liveMaxInstanceLives">Max simultaneous lives created on your instance</label>
|
<label i18n for="liveMaxInstanceLives">Max simultaneous lives created on your instance</label>
|
||||||
|
|
||||||
<span class="ms-2 small muted">(-1 for "unlimited")</span>
|
<span i18n class="ms-2 small muted">(-1 for "unlimited")</span>
|
||||||
|
|
||||||
<div class="number-with-unit">
|
<div class="number-with-unit">
|
||||||
<input type="number" id="liveMaxInstanceLives" formControlName="maxInstanceLives" />
|
<input type="number" id="liveMaxInstanceLives" formControlName="maxInstanceLives" />
|
||||||
|
@ -60,7 +60,7 @@
|
||||||
|
|
||||||
<div class="form-group" [ngClass]="getDisabledLiveClass()">
|
<div class="form-group" [ngClass]="getDisabledLiveClass()">
|
||||||
<label i18n for="liveMaxUserLives">Max simultaneous lives created per user</label>
|
<label i18n for="liveMaxUserLives">Max simultaneous lives created per user</label>
|
||||||
<span class="ms-2 small muted">(-1 for "unlimited")</span>
|
<span i18n class="ms-2 small muted">(-1 for "unlimited")</span>
|
||||||
|
|
||||||
<div class="number-with-unit">
|
<div class="number-with-unit">
|
||||||
<input type="number" id="liveMaxUserLives" formControlName="maxUserLives" />
|
<input type="number" id="liveMaxUserLives" formControlName="maxUserLives" />
|
||||||
|
@ -113,6 +113,19 @@
|
||||||
|
|
||||||
<div [ngClass]="getDisabledLiveTranscodingClass()">
|
<div [ngClass]="getDisabledLiveTranscodingClass()">
|
||||||
|
|
||||||
|
<div class="form-group" formGroupName="fps">
|
||||||
|
<label i18n for="liveTranscodingFPSMax">Max live FPS</label>
|
||||||
|
|
||||||
|
<span i18n class="ms-2 small muted">Cap transcoded live FPS. Max resolution stream still keeps the original FPS.</span>
|
||||||
|
|
||||||
|
<div class="number-with-unit">
|
||||||
|
<input type="number" name="liveTranscodingFPSMax" formControlName="max" />
|
||||||
|
<span>FPS</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="formErrors.live.transcoding.fps.max" class="form-error" role="alert">{{ formErrors.live.transcoding.fps.max }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="ms-2 mt-3">
|
<div class="ms-2 mt-3">
|
||||||
<h4 i18n>Live resolutions to generate</h4>
|
<h4 i18n>Live resolutions to generate</h4>
|
||||||
|
|
||||||
|
|
|
@ -141,6 +141,19 @@
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
<div class="form-group" formGroupName="fps" [ngClass]="getTranscodingDisabledClass()">
|
||||||
|
<label i18n for="transcodingFPSMax">Max video FPS</label>
|
||||||
|
|
||||||
|
<span i18n class="ms-2 small muted">Cap transcoded video FPS. Max resolution file still keeps the original FPS.</span>
|
||||||
|
|
||||||
|
<div class="number-with-unit">
|
||||||
|
<input type="number" name="transcodingFPSMax" formControlName="max" />
|
||||||
|
<span>FPS</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="formErrors.transcoding.fps.max" class="form-error" role="alert">{{ formErrors.transcoding.fps.max }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group" [ngClass]="getTranscodingDisabledClass()">
|
<div class="form-group" [ngClass]="getTranscodingDisabledClass()">
|
||||||
<div class="mb-2 fw-bold" i18n>Resolutions to generate</div>
|
<div class="mb-2 fw-bold" i18n>Resolutions to generate</div>
|
||||||
|
|
||||||
|
|
|
@ -65,6 +65,14 @@ export const TRANSCODING_THREADS_VALIDATOR: BuildFormValidator = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const TRANSCODING_MAX_FPS_VALIDATOR: BuildFormValidator = {
|
||||||
|
VALIDATORS: [ Validators.required, Validators.min(1) ],
|
||||||
|
MESSAGES: {
|
||||||
|
required: $localize`Transcoding max FPS is required.`,
|
||||||
|
min: $localize`Transcoding max FPS must be greater or equal to 1.`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const MAX_LIVE_DURATION_VALIDATOR: BuildFormValidator = {
|
export const MAX_LIVE_DURATION_VALIDATOR: BuildFormValidator = {
|
||||||
VALIDATORS: [ Validators.required, Validators.min(-1) ],
|
VALIDATORS: [ Validators.required, Validators.min(-1) ],
|
||||||
MESSAGES: {
|
MESSAGES: {
|
||||||
|
|
|
@ -155,25 +155,30 @@ export class AdvancedInputFilterComponent implements OnInit, AfterViewInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
private addFilterToSearch (search: string, newFilter: AdvancedInputFilterChild) {
|
private addFilterToSearch (search: string, newFilter: AdvancedInputFilterChild) {
|
||||||
const prefix = newFilter.value.split(':').shift()
|
const filterTokens = this.restService.tokenizeString(newFilter.value)
|
||||||
|
let searchTokens = this.restService.tokenizeString(search)
|
||||||
|
|
||||||
// Tokenize search and remove a potential existing filter
|
for (const filterToken of filterTokens) {
|
||||||
const tokens = this.restService.tokenizeString(search)
|
const prefix = filterToken.split(':').shift()
|
||||||
.filter(t => !t.startsWith(prefix))
|
|
||||||
|
|
||||||
tokens.push(newFilter.value)
|
// Tokenize search and remove a potential existing filter
|
||||||
|
searchTokens = searchTokens.filter(t => !t.startsWith(prefix))
|
||||||
|
searchTokens.push(filterToken)
|
||||||
|
}
|
||||||
|
|
||||||
return tokens.join(' ')
|
return searchTokens.join(' ')
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseFilters (search: string) {
|
private parseFilters (search: string) {
|
||||||
const tokens = this.restService.tokenizeString(search)
|
const searchTokens = this.restService.tokenizeString(search)
|
||||||
|
|
||||||
this.enabledFilters = new Set()
|
this.enabledFilters = new Set()
|
||||||
|
|
||||||
for (const group of this.filters) {
|
for (const group of this.filters) {
|
||||||
for (const filter of group.children) {
|
for (const filter of group.children) {
|
||||||
if (tokens.includes(filter.value)) {
|
const filterTokens = this.restService.tokenizeString(filter.value)
|
||||||
|
|
||||||
|
if (filterTokens.every(filterToken => searchTokens.includes(filterToken))) {
|
||||||
this.enabledFilters.add(filter.value)
|
this.enabledFilters.add(filter.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -382,14 +382,17 @@ export class VideoService {
|
||||||
generateDownloadUrl (options: {
|
generateDownloadUrl (options: {
|
||||||
video: Video
|
video: Video
|
||||||
files: VideoFile[]
|
files: VideoFile[]
|
||||||
|
videoFileToken?: string
|
||||||
}) {
|
}) {
|
||||||
const { video, files } = options
|
const { video, files, videoFileToken } = options
|
||||||
|
|
||||||
if (files.length === 0) throw new Error('Cannot generate download URL without files')
|
if (files.length === 0) throw new Error('Cannot generate download URL without files')
|
||||||
|
|
||||||
let url = `${VideoService.BASE_VIDEO_DOWNLOAD_URL}/${video.uuid}?`
|
let url = `${VideoService.BASE_VIDEO_DOWNLOAD_URL}/${video.uuid}?`
|
||||||
url += files.map(f => 'videoFileIds=' + f.id).join('&')
|
url += files.map(f => 'videoFileIds=' + f.id).join('&')
|
||||||
|
|
||||||
|
if (videoFileToken) url += `&videoFileToken=${videoFileToken}`
|
||||||
|
|
||||||
return url
|
return url
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -136,7 +136,7 @@ export class UserVideoSettingsComponent extends FormReactive implements OnInit,
|
||||||
next: () => {
|
next: () => {
|
||||||
this.authService.refreshUserInformation()
|
this.authService.refreshUserInformation()
|
||||||
|
|
||||||
if (this.notifyOnUpdate) this.notifier.success($localize`Video settings updated.`, 'toto', 15000)
|
if (this.notifyOnUpdate) this.notifier.success($localize`Video settings updated.`)
|
||||||
},
|
},
|
||||||
|
|
||||||
error: err => this.notifier.error(err.message)
|
error: err => this.notifier.error(err.message)
|
||||||
|
|
|
@ -104,7 +104,7 @@ export class VideoGenerateDownloadComponent implements OnInit {
|
||||||
files.push(this.findAudioFileOnly())
|
files.push(this.findAudioFileOnly())
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.videoService.generateDownloadUrl({ video: this.video, files })
|
return this.videoService.generateDownloadUrl({ video: this.video, videoFileToken: this.videoFileToken, files })
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
|
@ -45,7 +45,8 @@ export class VideoFilters {
|
||||||
[ 'categoryOneOf', undefined ],
|
[ 'categoryOneOf', undefined ],
|
||||||
[ 'scope', 'federated' ],
|
[ 'scope', 'federated' ],
|
||||||
[ 'allVideos', false ],
|
[ 'allVideos', false ],
|
||||||
[ 'live', 'both' ]
|
[ 'live', 'both' ],
|
||||||
|
[ 'search', '' ]
|
||||||
])
|
])
|
||||||
|
|
||||||
private activeFilters: VideoFilterActive[] = []
|
private activeFilters: VideoFilterActive[] = []
|
||||||
|
|
|
@ -502,7 +502,7 @@ export class PeerTubePlayer {
|
||||||
{
|
{
|
||||||
label: player.localize('Copy the video URL'),
|
label: player.localize('Copy the video URL'),
|
||||||
listener: function () {
|
listener: function () {
|
||||||
copyToClipboard(buildVideoLink({ shortUUID }))
|
copyToClipboard(buildVideoLink({ shortUUID }), player.el() as HTMLElement)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -510,17 +510,17 @@ export class PeerTubePlayer {
|
||||||
listener: function () {
|
listener: function () {
|
||||||
const url = buildVideoLink({ shortUUID })
|
const url = buildVideoLink({ shortUUID })
|
||||||
|
|
||||||
copyToClipboard(decorateVideoLink({ url, startTime: player.currentTime() }))
|
copyToClipboard(decorateVideoLink({ url, startTime: player.currentTime() }), player.el() as HTMLElement)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'code',
|
icon: 'code',
|
||||||
label: player.localize('Copy embed code'),
|
label: player.localize('Copy embed code'),
|
||||||
listener: () => {
|
listener: () => {
|
||||||
copyToClipboard(buildVideoOrPlaylistEmbed({
|
copyToClipboard(
|
||||||
embedUrl: self.currentLoadOptions.embedUrl,
|
buildVideoOrPlaylistEmbed({ embedUrl: self.currentLoadOptions.embedUrl, embedTitle: self.currentLoadOptions.embedTitle }),
|
||||||
embedTitle: self.currentLoadOptions.embedTitle
|
player.el() as HTMLElement
|
||||||
}))
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -36,12 +36,15 @@ class PeerTubeMobilePlugin extends Plugin {
|
||||||
|
|
||||||
this.seekAmount = 0
|
this.seekAmount = 0
|
||||||
|
|
||||||
this.peerTubeMobileButtons = player.addChild('PeerTubeMobileButtons', { reportTouchActivity: false }) as PeerTubeMobileButtons
|
|
||||||
|
|
||||||
if (videojs.browser.IS_ANDROID && screen.orientation) {
|
if (videojs.browser.IS_ANDROID && screen.orientation) {
|
||||||
this.handleFullscreenRotation()
|
this.handleFullscreenRotation()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Don't add buttons if the player doesn't have controls
|
||||||
|
if (!player.controls()) return
|
||||||
|
|
||||||
|
this.peerTubeMobileButtons = player.addChild('PeerTubeMobileButtons', { reportTouchActivity: false }) as PeerTubeMobileButtons
|
||||||
|
|
||||||
if (!this.player.options_.userActions) this.player.options_.userActions = {};
|
if (!this.player.options_.userActions) this.player.options_.userActions = {};
|
||||||
|
|
||||||
// FIXME: typings
|
// FIXME: typings
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
function copyToClipboard (text: string) {
|
function copyToClipboard (text: string, container?: HTMLElement) {
|
||||||
|
if (!container) container = document.body
|
||||||
|
|
||||||
const el = document.createElement('textarea')
|
const el = document.createElement('textarea')
|
||||||
el.value = text
|
el.value = text
|
||||||
el.setAttribute('readonly', '')
|
el.setAttribute('readonly', '')
|
||||||
el.style.position = 'absolute'
|
el.style.position = 'absolute'
|
||||||
el.style.left = '-9999px'
|
el.style.left = '-9999px'
|
||||||
document.body.appendChild(el)
|
container.appendChild(el)
|
||||||
el.select()
|
el.select()
|
||||||
document.execCommand('copy')
|
document.execCommand('copy')
|
||||||
document.body.removeChild(el)
|
container.removeChild(el)
|
||||||
}
|
}
|
||||||
|
|
||||||
function wait (ms: number) {
|
function wait (ms: number) {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "peertube",
|
"name": "peertube",
|
||||||
"description": "PeerTube, an ActivityPub-federated video streaming platform using P2P directly in your web browser.",
|
"description": "PeerTube, an ActivityPub-federated video streaming platform using P2P directly in your web browser.",
|
||||||
"version": "6.3.1",
|
"version": "6.3.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"licence": "AGPL-3.0",
|
"licence": "AGPL-3.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
|
@ -178,7 +178,14 @@ async function saveReplayToExternalVideo (options: {
|
||||||
inputFileMutexReleaser()
|
inputFileMutexReleaser()
|
||||||
}
|
}
|
||||||
|
|
||||||
await copyOrRegenerateThumbnails({ liveVideo, replayVideo })
|
try {
|
||||||
|
await copyOrRegenerateThumbnails({ liveVideo, replayVideo })
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(
|
||||||
|
`Cannot copy/regenerate thumbnails of ended live ${liveVideo.uuid} to external video ${replayVideo.uuid}`,
|
||||||
|
lTags(liveVideo.uuid, replayVideo.uuid)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
await createStoryboardJob(replayVideo)
|
await createStoryboardJob(replayVideo)
|
||||||
await createTranscriptionTaskIfNeeded(replayVideo)
|
await createTranscriptionTaskIfNeeded(replayVideo)
|
||||||
|
@ -280,7 +287,11 @@ async function replaceLiveByReplay (options: {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Regenerate the thumbnail & preview?
|
// Regenerate the thumbnail & preview?
|
||||||
await regenerateMiniaturesIfNeeded(videoWithFiles, undefined)
|
try {
|
||||||
|
await regenerateMiniaturesIfNeeded(videoWithFiles, undefined)
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(`Cannot regenerate thumbnails of ended live ${videoWithFiles.uuid}`, lTags(liveVideo.uuid))
|
||||||
|
}
|
||||||
|
|
||||||
// We consider this is a new video
|
// We consider this is a new video
|
||||||
await moveToNextState({ video: videoWithFiles, isNewVideo: true })
|
await moveToNextState({ video: videoWithFiles, isNewVideo: true })
|
||||||
|
|
|
@ -284,6 +284,7 @@ function buildTags (video: MVideoAP) {
|
||||||
|
|
||||||
function buildIcon (video: MVideoAP): ActivityIconObject[] {
|
function buildIcon (video: MVideoAP): ActivityIconObject[] {
|
||||||
return [ video.getMiniature(), video.getPreview() ]
|
return [ video.getMiniature(), video.getPreview() ]
|
||||||
|
.filter(i => !!i)
|
||||||
.map(i => i.toActivityPubObject(video))
|
.map(i => i.toActivityPubObject(video))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue