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=2d3fc3578fb32eb1269eee57df8522911b842fe9;hb=f6d6e7f861189a4446f406efb775a29688764b48;hp=effb37077465c3d7b9c3b913b3c034c7255e4e41;hpb=dc48fdbe68e9dd3a3a6028181e61d8595d98e654;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 effb37077..2d3fc3578 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,15 +1,16 @@ -import { Subscription } from 'rxjs' -import { HttpErrorResponse, HttpEventType, HttpResponse } from '@angular/common/http' import { AfterViewInit, Component, ElementRef, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core' import { Router } from '@angular/router' +import { UploadxOptions, UploadState, UploadxService } from 'ngx-uploadx' +import { UploaderXFormData } from './uploaderx-form-data' import { AuthService, CanComponentDeactivate, HooksService, Notifier, ServerService, UserService } from '@app/core' -import { scrollToTop, uploadErrorHandler } from '@app/helpers' +import { scrollToTop, genericUploadErrorHandler } from '@app/helpers' import { FormValidatorService } from '@app/shared/shared-forms' import { BytesPipe, 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 { VideoSend } from './video-send' +import { HttpErrorResponse, HttpEventType, HttpHeaders } from '@angular/common/http' @Component({ selector: 'my-video-upload', @@ -20,23 +21,18 @@ import { VideoSend } from './video-send' './video-send.scss' ] }) -export class VideoUploadComponent extends VideoSend implements OnInit, AfterViewInit, 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, @@ -49,7 +45,13 @@ export class VideoUploadComponent extends VideoSend implements OnInit, AfterView error: string enableRetryAfterError: boolean + // So that it can be accessed in the template protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC + 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, @@ -61,15 +63,77 @@ export class VideoUploadComponent extends VideoSend implements OnInit, AfterView protected videoCaptionService: VideoCaptionService, private userService: UserService, private router: Router, - private hooks: HooksService - ) { + 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 cancelled`) + break + + case 'complete': + this.videoUploaded = true + this.videoUploadPercents = 100 + + this.videoUploadedIds = state?.response.video + break + } + } + ngOnInit () { super.ngOnInit() @@ -78,6 +142,9 @@ export class VideoUploadComponent extends VideoSend implements OnInit, AfterView this.userVideoQuotaUsed = data.videoQuotaUsed this.userVideoQuotaUsedDaily = data.videoQuotaUsedDaily }) + + this.resumableUploadService.events + .subscribe(state => this.onUploadVideoOngoing(state)) } ngAfterViewInit () { @@ -85,7 +152,7 @@ export class VideoUploadComponent extends VideoSend implements OnInit, AfterView } ngOnDestroy () { - if (this.videoUploadObservable) this.videoUploadObservable.unsubscribe() + this.cancelUpload() } canDeactivate () { @@ -105,137 +172,43 @@ export class VideoUploadComponent extends VideoSend implements OnInit, AfterView } } - 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}` + this.onFileChange({ target: this.videofileInput.nativeElement }) } - fileChange () { - this.uploadFirstStep() - } - - 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 + onFileChange (event: Event | { target: HTMLInputElement }) { + const file = (event.target as HTMLInputElement).files[0] - this.firstStepError.emit() - this.enableRetryAfterError = false - this.error = '' + if (!file) return - this.notifier.info($localize`Upload cancelled`) - } + if (!this.checkGlobalUserQuota(file)) return + if (!this.checkDailyUserQuota(file)) return - uploadFirstStep (clickedOnButton = false) { - const videofile = this.getVideoFile() - if (!videofile) 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 - - 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 - - this.videoUploadObservable = null - } - }, + uploadAudio () { + this.uploadFile(this.getInputVideoFile(), this.previewfileUpload) + } - (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 - }) + retryUpload () { + this.enableRetryAfterError = false + this.error = '' + this.uploadFile(this.fileToUpload) + } - if (err.status === HttpStatusCode.PAYLOAD_TOO_LARGE_413 || - err.status === HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415) { - this.cancelUpload() - } - } - ) + cancelUpload () { + this.resumableUploadService.control({ action: 'cancel' }) } isPublishingButtonDisabled () { @@ -245,6 +218,13 @@ export class VideoUploadComponent extends VideoSend implements OnInit, AfterView !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 @@ -275,6 +255,62 @@ export class VideoUploadComponent extends VideoSend implements OnInit, AfterView ) } + 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: VideoPrivacy.PRIVATE.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() @@ -285,8 +321,7 @@ export class VideoUploadComponent extends VideoSend implements OnInit, AfterView 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 @@ -304,9 +339,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