diff --git a/client/src/app/+videos/+video-edit/shared/thumbnail-manager/thumbnail-manager.component.html b/client/src/app/+videos/+video-edit/shared/thumbnail-manager/thumbnail-manager.component.html index 56341290e..43cd7d45a 100644 --- a/client/src/app/+videos/+video-edit/shared/thumbnail-manager/thumbnail-manager.component.html +++ b/client/src/app/+videos/+video-edit/shared/thumbnail-manager/thumbnail-manager.component.html @@ -1,41 +1,37 @@
-
- - @if (!selectingFromVideo) { - + @if (video && selectingFromVideo) { + + } @else {
- @if (imageSrc) { - Preview - } - @else { + Preview + } @else {
}
- } + } -
+
@if (selectingFromVideo) { + - - - - + } @else { - - - - + @if (video) { + + } }
-
\ No newline at end of file +
diff --git a/client/src/app/+videos/+video-edit/shared/thumbnail-manager/thumbnail-manager.component.scss b/client/src/app/+videos/+video-edit/shared/thumbnail-manager/thumbnail-manager.component.scss index d9e795a4b..690e8d9dd 100644 --- a/client/src/app/+videos/+video-edit/shared/thumbnail-manager/thumbnail-manager.component.scss +++ b/client/src/app/+videos/+video-edit/shared/thumbnail-manager/thumbnail-manager.component.scss @@ -5,50 +5,27 @@ height: auto; display: flex; flex-direction: column; +} - .preview-container { - position: relative; +.preview-container { + position: relative; - .preview { - object-fit: cover; - border-radius: 4px; - max-width: 100%; + .preview { + object-fit: cover; + border-radius: 4px; - &.no-image { - border: 2px solid #808080; - background-color: pvar(--mainBackgroundColor); - } + &.no-image { + border: 2px solid #808080; + background-color: pvar(--mainBackgroundColor); } } +} - .inputs { +.preview, +my-embed { + max-width: 500px; + width: 500px; - margin-top: 10px; - - my-reactive-file { - display: inline-block; - margin-right: 10px; - } - - input { - display: inline-block; - margin-right: 10px; - } - - } - - .video-embed { - - $video-default-height: 40vh; - - --player-height: #{$video-default-height}; - // Default player ratio, redefined by the player to automatically adapt player size - --player-ratio: #{math.div(16, 9)}; - - width: 100%; - height: var(--player-height); - - // Can be recalculated by the player depending on video ratio - max-width: calc(var(--player-height) * var(--player-ratio)); - } -} \ No newline at end of file + max-height: 280px; + height: 280px; +} diff --git a/client/src/app/+videos/+video-edit/shared/thumbnail-manager/thumbnail-manager.component.ts b/client/src/app/+videos/+video-edit/shared/thumbnail-manager/thumbnail-manager.component.ts index e91e9c703..f82f4f0a4 100644 --- a/client/src/app/+videos/+video-edit/shared/thumbnail-manager/thumbnail-manager.component.ts +++ b/client/src/app/+videos/+video-edit/shared/thumbnail-manager/thumbnail-manager.component.ts @@ -1,28 +1,28 @@ -import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' import { CommonModule } from '@angular/common' -import { imageToDataURL } from '@root-helpers/images' -import { BytesPipe } from '@app/shared/shared-main/angular/bytes.pipe' - import { Component, - forwardRef, Input, - OnInit + OnInit, + ViewChild, + forwardRef } from '@angular/core' +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' import { ServerService } from '@app/core' -import { HTMLServerConfig } from '@peertube/peertube-models' import { ReactiveFileComponent } from '@app/shared/shared-forms/reactive-file.component' -import { PeerTubePlayer } from 'src/standalone/embed-player-api/player' -import { getAbsoluteAPIUrl } from '@app/helpers' +import { BytesPipe } from '@app/shared/shared-main/angular/bytes.pipe' +import { EmbedComponent, EmbedVideoInput } from '@app/shared/shared-main/video/embed.component' +import { HTMLServerConfig } from '@peertube/peertube-models' +import { imageToDataURL } from '@root-helpers/images' +import { PeerTubePlayer } from '../../../../../standalone/embed-player-api/player' @Component({ selector: 'my-thumbnail-manager', styleUrls: [ './thumbnail-manager.component.scss' ], templateUrl: './thumbnail-manager.component.html', standalone: true, - imports: [ CommonModule, ReactiveFileComponent ], + imports: [ CommonModule, ReactiveFileComponent, EmbedComponent ], providers: [ { provide: NG_VALUE_ACCESSOR, @@ -32,11 +32,9 @@ import { getAbsoluteAPIUrl } from '@app/helpers' ] }) export class ThumbnailManagerComponent implements OnInit, ControlValueAccessor { + @ViewChild('embed') embed: EmbedComponent - @Input() uuid: string - - previewWidth = '360px' - previewHeight = '200px' + @Input() video: EmbedVideoInput imageSrc: string allowedExtensionsMessage = '' @@ -46,7 +44,6 @@ export class ThumbnailManagerComponent implements OnInit, ControlValueAccessor { bytesPipe: BytesPipe imageFile: Blob - // State Toggle (Upload, Select Frame) selectingFromVideo = false player: PeerTubePlayer @@ -58,7 +55,10 @@ export class ThumbnailManagerComponent implements OnInit, ControlValueAccessor { this.maxSizeText = $localize`max size` } - // Section - Upload + // --------------------------------------------------------------------------- + // Upload + // --------------------------------------------------------------------------- + get videoImageExtensions () { return this.serverConfig.video.image.extensions } @@ -108,44 +108,29 @@ export class ThumbnailManagerComponent implements OnInit, ControlValueAccessor { imageToDataURL(this.imageFile).then(result => this.imageSrc = result) } } - // End Section - Upload - // Section - Select From Frame + // --------------------------------------------------------------------------- + // Select from frame + // --------------------------------------------------------------------------- + selectFromVideo () { - this.selectingFromVideo = true - const url = getAbsoluteAPIUrl() - - const iframe = document.createElement('iframe') - iframe.src = `${url}/videos/embed/${this.uuid}?api=1&waitPasswordFromEmbedAPI=1&muted=1&title=0&peertubeLink=0` - - iframe.sandbox.add('allow-same-origin', 'allow-scripts', 'allow-popups') - - iframe.height = '100%' - iframe.width = '100%' - - const mainElement = document.querySelector('#embedContainer') - mainElement.appendChild(iframe) - - mainElement.classList.add('video-embed') - - this.player = new PeerTubePlayer(iframe) + setTimeout(() => { + this.player = new PeerTubePlayer(this.embed.getIframe()) + }) } resetSelectFromVideo () { - - if (this.player) this.player.destroy() - - const mainElement = document.querySelector('#embedContainer') - - mainElement.classList.remove('video-embed') + if (this.player) { + this.player.destroy() + this.player = undefined + } this.selectingFromVideo = false } async selectFrame () { - const dataUrl: string = await this.player.getImageDataUrl() // Checking for an empty data URL @@ -157,42 +142,28 @@ export class ThumbnailManagerComponent implements OnInit, ControlValueAccessor { const blob: Blob = this.dataURItoBlob(dataUrl) - const file = new File([ blob ], 'PreviewFile.jpg', { type: 'image/jpeg' }) + const file = new File([ blob ], 'preview-file-from-frame.jpg', { type: 'image/jpeg' }) this.imageFile = file this.propagateChange(this.imageFile) this.resetSelectFromVideo() - } - /* - * Credit: https://stackoverflow.com/a/7261048/1030669 - */ + // Credit: https://stackoverflow.com/a/7261048/1030669 dataURItoBlob (dataURI: string) { - // convert base64 to raw binary data held in a string - // doesn't handle URLEncoded DataURIs - see SO answer #6850276 for code that does this const byteString = atob(dataURI.split(',')[1]) - - // separate out the mime component const mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0] - // write the bytes of the string to an ArrayBuffer const ab = new ArrayBuffer(byteString.length) - // create a view into the buffer const ia = new Uint8Array(ab) - // set the bytes of the buffer to the correct values for (let i = 0; i < byteString.length; i++) { ia[i] = byteString.charCodeAt(i) } - // write the ArrayBuffer to a blob, and you're done - const blob = new Blob([ ab ], { type: mimeString }) - return blob - + return new Blob([ ab ], { type: mimeString }) } - // End Section - Upload } diff --git a/client/src/app/+videos/+video-edit/shared/video-edit.component.html b/client/src/app/+videos/+video-edit/shared/video-edit.component.html index 8e93cc923..9bc160bbf 100644 --- a/client/src/app/+videos/+video-edit/shared/video-edit.component.html +++ b/client/src/app/+videos/+video-edit/shared/video-edit.component.html @@ -54,7 +54,7 @@ @@ -252,7 +252,7 @@
@@ -276,7 +276,7 @@
- + @@ -375,7 +375,7 @@
- +
diff --git a/client/src/app/+videos/+video-edit/shared/video-edit.component.ts b/client/src/app/+videos/+video-edit/shared/video-edit.component.ts index 118dc6b51..a6aad3951 100644 --- a/client/src/app/+videos/+video-edit/shared/video-edit.component.ts +++ b/client/src/app/+videos/+video-edit/shared/video-edit.component.ts @@ -119,7 +119,7 @@ export class VideoEditComponent implements OnInit, OnDestroy { @Input() formErrors: FormReactiveErrors & { chapters?: { title: string }[] } = {} @Input() validationMessages: FormReactiveValidationMessages = {} - @Input() videoToUpdate: VideoDetails + @Input() publishedVideo: VideoDetails @Input() userVideoChannels: SelectChannelItem[] = [] @Input() forbidScheduledPublication = true @@ -418,7 +418,7 @@ export class VideoEditComponent implements OnInit, OnDestroy { return pluginField.commonOptions.hidden({ formValues: this.form.value, - videoToUpdate: this.videoToUpdate, + videoToUpdate: this.publishedVideo, liveVideo: this.liveVideo }) } diff --git a/client/src/app/+videos/+video-edit/video-update.component.html b/client/src/app/+videos/+video-edit/video-update.component.html index 78ae5357a..0761d4c09 100644 --- a/client/src/app/+videos/+video-edit/video-update.component.html +++ b/client/src/app/+videos/+video-edit/video-update.component.html @@ -18,7 +18,7 @@ [validationMessages]="validationMessages" [userVideoChannels]="userVideoChannels" [videoCaptions]="videoCaptions" [hideWaitTranscoding]="isWaitTranscodingHidden()" type="update" (pluginFieldsAdded)="hydratePluginFieldsFromVideo()" - [liveVideo]="liveVideo" [videoToUpdate]="videoDetails" + [liveVideo]="liveVideo" [publishedVideo]="videoDetails" [videoSource]="videoSource" [updateVideoFileEnabled]="isUpdateVideoFileEnabled()" (formBuilt)="onFormBuilt()" diff --git a/client/src/app/shared/shared-main/video/embed.component.ts b/client/src/app/shared/shared-main/video/embed.component.ts index e5166e8ce..248416a74 100644 --- a/client/src/app/shared/shared-main/video/embed.component.ts +++ b/client/src/app/shared/shared-main/video/embed.component.ts @@ -1,10 +1,12 @@ import { environment } from 'src/environments/environment' -import { Component, Input, OnInit } from '@angular/core' +import { Component, ElementRef, Input, OnInit, booleanAttribute } from '@angular/core' import { DomSanitizer, SafeHtml } from '@angular/platform-browser' import { buildVideoOrPlaylistEmbed } from '@root-helpers/video' import { buildVideoEmbedLink, decorateVideoLink } from '@peertube/peertube-core-utils' import { Video } from '@peertube/peertube-models' +export type EmbedVideoInput = Pick & Partial> + @Component({ selector: 'my-embed', styleUrls: [ './embed.component.scss' ], @@ -12,11 +14,17 @@ import { Video } from '@peertube/peertube-models' standalone: true }) export class EmbedComponent implements OnInit { - @Input({ required: true }) video: Pick & Partial> + @Input({ required: true }) video: EmbedVideoInput + @Input({ transform: booleanAttribute }) enableAPI: boolean + @Input({ transform: booleanAttribute }) mute: boolean + @Input({ transform: booleanAttribute }) autoplay: boolean embedHTML: SafeHtml - constructor (private sanitizer: DomSanitizer) { + constructor ( + private sanitizer: DomSanitizer, + private el: ElementRef + ) { } @@ -26,7 +34,10 @@ export class EmbedComponent implements OnInit { url: buildVideoEmbedLink(this.video, environment.originServerUrl), title: false, - warningTitle: false + warningTitle: false, + api: this.enableAPI, + muted: this.mute, + autoplay: this.autoplay }), embedTitle: this.video.name, aspectRatio: this.video.aspectRatio @@ -34,4 +45,8 @@ export class EmbedComponent implements OnInit { this.embedHTML = this.sanitizer.bypassSecurityTrustHtml(html) } + + getIframe () { + return (this.el.nativeElement as HTMLElement).querySelector('iframe') + } } diff --git a/packages/core-utils/src/common/url.ts b/packages/core-utils/src/common/url.ts index 9b1b0650f..47eaecdbe 100644 --- a/packages/core-utils/src/common/url.ts +++ b/packages/core-utils/src/common/url.ts @@ -84,6 +84,8 @@ function decorateVideoLink (options: { peertubeLink?: boolean p2p?: boolean + + api?: boolean }) { const { url } = options @@ -113,6 +115,8 @@ function decorateVideoLink (options: { if (options.peertubeLink === false) params.set('peertubeLink', '0') if (options.p2p !== undefined) params.set('p2p', options.p2p ? '1' : '0') + if (options.api !== undefined) params.set('api', options.api ? '1' : '0') + return buildUrl(url, params) }