X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=client%2Fsrc%2Fapp%2F%2Bvideos%2F%2Bvideo-edit%2Fvideo-add-components%2Fvideo-upload.component.ts;h=627de33c04b46da4a48c4e3582bf9b346d822b81;hb=08642a765ea514a00f159db898edf14c376fbe6c;hp=fdd0a56e5dc2942ace73d69ca8b750dac1216da4;hpb=d4132d3f56b392a2e4e632db59e6644e4851228e;p=github%2FChocobozzz%2FPeerTube.git diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts index fdd0a56e5..627de33c0 100644 --- a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts +++ b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts @@ -1,13 +1,15 @@ -import { Subscription } from 'rxjs' -import { HttpErrorResponse, HttpEventType, HttpResponse } from '@angular/common/http' -import { Component, ElementRef, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core' +import { UploadState, UploadxOptions, UploadxService } from 'ngx-uploadx' +import { HttpErrorResponse, HttpEventType, HttpHeaders } from '@angular/common/http' +import { AfterViewInit, Component, ElementRef, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core' import { Router } from '@angular/router' -import { AuthService, CanComponentDeactivate, Notifier, ServerService, UserService } from '@app/core' -import { scrollToTop, uploadErrorHandler } from '@app/helpers' +import { AuthService, CanComponentDeactivate, HooksService, Notifier, ServerService, UserService } from '@app/core' +import { genericUploadErrorHandler, scrollToTop } from '@app/helpers' import { FormValidatorService } from '@app/shared/shared-forms' -import { BytesPipe, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main' +import { BytesPipe, Video, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main' import { LoadingBarService } from '@ngx-loading-bar/core' +import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' import { VideoPrivacy } from '@shared/models' +import { UploaderXFormData } from './uploaderx-form-data' import { VideoSend } from './video-send' @Component({ @@ -19,23 +21,18 @@ import { VideoSend } from './video-send' './video-send.scss' ] }) -export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy, CanComponentDeactivate { +export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy, AfterViewInit, CanComponentDeactivate { @Output() firstStepDone = new EventEmitter() @Output() firstStepError = new EventEmitter() @ViewChild('videofileInput') videofileInput: ElementRef - // So that it can be accessed in the template - readonly SPECIAL_SCHEDULED_PRIVACY = VideoEdit.SPECIAL_SCHEDULED_PRIVACY - userVideoQuotaUsed = 0 userVideoQuotaUsedDaily = 0 isUploadingAudioFile = false isUploadingVideo = false - isUpdatingVideo = false videoUploaded = false - videoUploadObservable: Subscription = null videoUploadPercents = 0 videoUploadedIds = { id: 0, @@ -43,13 +40,17 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy } formData: FormData - waitTranscodingEnabled = true previewfileUpload: File error: string enableRetryAfterError: boolean - protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC + // So that it can be accessed in the template + protected readonly BASE_VIDEO_UPLOAD_URL = VideoService.BASE_VIDEO_URL + 'upload-resumable' + + private uploadxOptions: UploadxOptions + private isUpdatingVideo = false + private fileToUpload: File constructor ( protected formValidatorService: FormValidatorService, @@ -60,15 +61,78 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy protected videoService: VideoService, protected videoCaptionService: VideoCaptionService, private userService: UserService, - private router: Router - ) { + private router: Router, + private hooks: HooksService, + private resumableUploadService: UploadxService + ) { super() + + this.uploadxOptions = { + endpoint: this.BASE_VIDEO_UPLOAD_URL, + multiple: false, + token: this.authService.getAccessToken(), + uploaderClass: UploaderXFormData, + retryConfig: { + maxAttempts: 6, + shouldRetry: (code: number) => { + return code < 400 || code >= 501 + } + } + } } get videoExtensions () { return this.serverConfig.video.file.extensions.join(', ') } + onUploadVideoOngoing (state: UploadState) { + switch (state.status) { + case 'error': + const error = state.response?.error || 'Unknow error' + + this.handleUploadError({ + error: new Error(error), + name: 'HttpErrorResponse', + message: error, + ok: false, + headers: new HttpHeaders(state.responseHeaders), + status: +state.responseStatus, + statusText: error, + type: HttpEventType.Response, + url: state.url + }) + break + + case 'cancelled': + this.isUploadingVideo = false + this.videoUploadPercents = 0 + + this.firstStepError.emit() + this.enableRetryAfterError = false + this.error = '' + break + + case 'queue': + this.closeFirstStep(state.name) + break + + case 'uploading': + this.videoUploadPercents = state.progress + break + + case 'paused': + this.notifier.info($localize`Upload on hold`) + break + + case 'complete': + this.videoUploaded = true + this.videoUploadPercents = 100 + + this.videoUploadedIds = state?.response.video + break + } + } + ngOnInit () { super.ngOnInit() @@ -77,17 +141,24 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy this.userVideoQuotaUsed = data.videoQuotaUsed this.userVideoQuotaUsedDaily = data.videoQuotaUsedDaily }) + + this.resumableUploadService.events + .subscribe(state => this.onUploadVideoOngoing(state)) + } + + ngAfterViewInit () { + this.hooks.runAction('action:video-upload.init', 'video-edit') } ngOnDestroy () { - if (this.videoUploadObservable) this.videoUploadObservable.unsubscribe() + this.cancelUpload() } canDeactivate () { let text = '' if (this.videoUploaded === true) { - // FIXME: cannot concatenate strings inside i18n service :/ + // FIXME: cannot concatenate strings using $localize text = $localize`Your video was uploaded to your account and is private.` + ' ' + $localize`But associated data (tags, description...) will be lost, are you sure you want to leave this page?` } else { @@ -100,137 +171,43 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy } } - getVideoFile () { - return this.videofileInput.nativeElement.files[0] - } - - setVideoFile (files: FileList) { + onFileDropped (files: FileList) { this.videofileInput.nativeElement.files = files - this.fileChange() - } - - getAudioUploadLabel () { - const videofile = this.getVideoFile() - if (!videofile) return $localize`Upload` - - return $localize`Upload ${videofile.name}` - } - fileChange () { - this.uploadFirstStep() + this.onFileChange({ target: this.videofileInput.nativeElement }) } - retryUpload () { - this.enableRetryAfterError = false - this.error = '' - this.uploadVideo() - } - - cancelUpload () { - if (this.videoUploadObservable !== null) { - this.videoUploadObservable.unsubscribe() - - this.isUploadingVideo = false - this.videoUploadPercents = 0 - this.videoUploadObservable = null - - this.firstStepError.emit() - this.enableRetryAfterError = false - this.error = '' + onFileChange (event: Event | { target: HTMLInputElement }) { + const file = (event.target as HTMLInputElement).files[0] - this.notifier.info($localize`Upload cancelled`) - } - } + if (!file) return - uploadFirstStep (clickedOnButton = false) { - const videofile = this.getVideoFile() - if (!videofile) return + if (!this.checkGlobalUserQuota(file)) return + if (!this.checkDailyUserQuota(file)) return - if (!this.checkGlobalUserQuota(videofile)) return - if (!this.checkDailyUserQuota(videofile)) return - - if (clickedOnButton === false && this.isAudioFile(videofile.name)) { + if (this.isAudioFile(file.name)) { this.isUploadingAudioFile = true return } - // Build name field - const nameWithoutExtension = videofile.name.replace(/\.[^/.]+$/, '') - let name: string - - // If the name of the file is very small, keep the extension - if (nameWithoutExtension.length < 3) name = videofile.name - else name = nameWithoutExtension - - // Force user to wait transcoding for unsupported video types in web browsers - if (!videofile.name.endsWith('.mp4') && !videofile.name.endsWith('.webm') && !videofile.name.endsWith('.ogv')) { - this.waitTranscodingEnabled = false - } - - const nsfw = this.serverConfig.instance.isNSFW - const waitTranscoding = true - const commentsEnabled = true - const downloadEnabled = true - const channelId = this.firstStepChannelId.toString() - - this.formData = new FormData() - this.formData.append('name', name) - // Put the video "private" -> we are waiting the user validation of the second step - this.formData.append('privacy', VideoPrivacy.PRIVATE.toString()) - this.formData.append('nsfw', '' + nsfw) - this.formData.append('commentsEnabled', '' + commentsEnabled) - this.formData.append('downloadEnabled', '' + downloadEnabled) - this.formData.append('waitTranscoding', '' + waitTranscoding) - this.formData.append('channelId', '' + channelId) - this.formData.append('videofile', videofile) - - if (this.previewfileUpload) { - this.formData.append('previewfile', this.previewfileUpload) - this.formData.append('thumbnailfile', this.previewfileUpload) - } - this.isUploadingVideo = true - this.firstStepDone.emit(name) - - this.form.patchValue({ - name, - privacy: this.firstStepPrivacyId, - nsfw, - channelId: this.firstStepChannelId, - previewfile: this.previewfileUpload - }) + this.fileToUpload = file - this.uploadVideo() + this.uploadFile(file) } - uploadVideo () { - this.videoUploadObservable = this.videoService.uploadVideo(this.formData).subscribe( - event => { - if (event.type === HttpEventType.UploadProgress) { - this.videoUploadPercents = Math.round(100 * event.loaded / event.total) - } else if (event instanceof HttpResponse) { - this.videoUploaded = true - - this.videoUploadedIds = event.body.video + uploadAudio () { + this.uploadFile(this.getInputVideoFile(), this.previewfileUpload) + } - this.videoUploadObservable = null - } - }, + retryUpload () { + this.enableRetryAfterError = false + this.error = '' + this.uploadFile(this.fileToUpload) + } - (err: HttpErrorResponse) => { - // Reset progress (but keep isUploadingVideo true) - this.videoUploadPercents = 0 - this.videoUploadObservable = null - this.enableRetryAfterError = true - - this.error = uploadErrorHandler({ - err, - name: $localize`video`, - notifier: this.notifier, - sticky: false - }) - } - ) + cancelUpload () { + this.resumableUploadService.control({ action: 'cancel' }) } isPublishingButtonDisabled () { @@ -240,6 +217,13 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy !this.videoUploadedIds.id } + getAudioUploadLabel () { + const videofile = this.getInputVideoFile() + if (!videofile) return $localize`Upload` + + return $localize`Upload ${videofile.name}` + } + updateSecondStep () { if (this.isPublishingButtonDisabled() || !this.checkForm()) { return @@ -259,7 +243,7 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy this.isUploadingVideo = false this.notifier.success($localize`Video published.`) - this.router.navigate([ '/videos/watch', video.uuid ]) + this.router.navigateByUrl(Video.buildWatchUrl(video)) }, err => { @@ -270,6 +254,62 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy ) } + private getInputVideoFile () { + return this.videofileInput.nativeElement.files[0] + } + + private uploadFile (file: File, previewfile?: File) { + const metadata = { + waitTranscoding: true, + commentsEnabled: true, + downloadEnabled: true, + channelId: this.firstStepChannelId, + nsfw: this.serverConfig.instance.isNSFW, + privacy: this.highestPrivacy.toString(), + filename: file.name, + previewfile: previewfile as any + } + + this.resumableUploadService.handleFiles(file, { + ...this.uploadxOptions, + metadata + }) + + this.isUploadingVideo = true + } + + private handleUploadError (err: HttpErrorResponse) { + // Reset progress (but keep isUploadingVideo true) + this.videoUploadPercents = 0 + this.enableRetryAfterError = true + + this.error = genericUploadErrorHandler({ + err, + name: $localize`video`, + notifier: this.notifier, + sticky: false + }) + + if (err.status === HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415) { + this.cancelUpload() + } + } + + private closeFirstStep (filename: string) { + const nameWithoutExtension = filename.replace(/\.[^/.]+$/, '') + const name = nameWithoutExtension.length < 3 ? filename : nameWithoutExtension + + this.form.patchValue({ + name, + privacy: this.firstStepPrivacyId, + nsfw: this.serverConfig.instance.isNSFW, + channelId: this.firstStepChannelId, + previewfile: this.previewfileUpload + }) + + this.firstStepDone.emit(name) + } + private checkGlobalUserQuota (videofile: File) { const bytePipes = new BytesPipe() @@ -280,8 +320,7 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy const videoQuotaUsedBytes = bytePipes.transform(this.userVideoQuotaUsed, 0) const videoQuotaBytes = bytePipes.transform(videoQuota, 0) - const msg = $localize`Your video quota is exceeded with this video ( -video size: ${videoSizeBytes}, used: ${videoQuotaUsedBytes}, quota: ${videoQuotaBytes})` + const msg = $localize`Your video quota is exceeded with this video (video size: ${videoSizeBytes}, used: ${videoQuotaUsedBytes}, quota: ${videoQuotaBytes})` this.notifier.error(msg) return false @@ -299,9 +338,7 @@ video size: ${videoSizeBytes}, used: ${videoQuotaUsedBytes}, quota: ${videoQuota const videoSizeBytes = bytePipes.transform(videofile.size, 0) const quotaUsedDailyBytes = bytePipes.transform(this.userVideoQuotaUsedDaily, 0) const quotaDailyBytes = bytePipes.transform(videoQuotaDaily, 0) - - const msg = $localize`Your daily video quota is exceeded with this video ( -video size: ${videoSizeBytes}, used: ${quotaUsedDailyBytes}, quota: ${quotaDailyBytes})` + const msg = $localize`Your daily video quota is exceeded with this video (video size: ${videoSizeBytes}, used: ${quotaUsedDailyBytes}, quota: ${quotaDailyBytes})` this.notifier.error(msg) return false