aboutsummaryrefslogtreecommitdiffhomepage
path: root/shared
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2022-02-11 10:51:33 +0100
committerChocobozzz <chocobozzz@cpy.re>2022-02-28 10:42:19 +0100
commitc729caf6cc34630877a0e5a1bda1719384cd0c8a (patch)
tree1d2e13722e518c73d2c9e6f0969615e29d51cf8c /shared
parenta24bf4dc659cebb65d887862bf21d7a35e9ec791 (diff)
downloadPeerTube-c729caf6cc34630877a0e5a1bda1719384cd0c8a.tar.gz
PeerTube-c729caf6cc34630877a0e5a1bda1719384cd0c8a.tar.zst
PeerTube-c729caf6cc34630877a0e5a1bda1719384cd0c8a.zip
Add basic video editor support
Diffstat (limited to 'shared')
-rw-r--r--shared/extra-utils/ffprobe.ts96
-rw-r--r--shared/models/server/custom-config.model.ts4
-rw-r--r--shared/models/server/job.model.ts39
-rw-r--r--shared/models/server/server-config.model.ts4
-rw-r--r--shared/models/videos/editor/index.ts1
-rw-r--r--shared/models/videos/editor/video-editor-create-edit.model.ts42
-rw-r--r--shared/models/videos/index.ts1
-rw-r--r--shared/models/videos/transcoding/video-transcoding-fps.model.ts1
-rw-r--r--shared/models/videos/transcoding/video-transcoding.model.ts7
-rw-r--r--shared/models/videos/video-state.enum.ts3
-rw-r--r--shared/server-commands/server/config-command.ts34
-rw-r--r--shared/server-commands/server/server.ts3
-rw-r--r--shared/server-commands/videos/index.ts1
-rw-r--r--shared/server-commands/videos/video-editor-command.ts67
14 files changed, 244 insertions, 59 deletions
diff --git a/shared/extra-utils/ffprobe.ts b/shared/extra-utils/ffprobe.ts
index 53a3aa001..dfacd251c 100644
--- a/shared/extra-utils/ffprobe.ts
+++ b/shared/extra-utils/ffprobe.ts
@@ -17,12 +17,22 @@ function ffprobePromise (path: string) {
17 }) 17 })
18} 18}
19 19
20// ---------------------------------------------------------------------------
21// Audio
22// ---------------------------------------------------------------------------
23
20async function isAudioFile (path: string, existingProbe?: FfprobeData) { 24async function isAudioFile (path: string, existingProbe?: FfprobeData) {
21 const videoStream = await getVideoStreamFromFile(path, existingProbe) 25 const videoStream = await getVideoStream(path, existingProbe)
22 26
23 return !videoStream 27 return !videoStream
24} 28}
25 29
30async function hasAudioStream (path: string, existingProbe?: FfprobeData) {
31 const { audioStream } = await getAudioStream(path, existingProbe)
32
33 return !!audioStream
34}
35
26async function getAudioStream (videoPath: string, existingProbe?: FfprobeData) { 36async function getAudioStream (videoPath: string, existingProbe?: FfprobeData) {
27 // without position, ffprobe considers the last input only 37 // without position, ffprobe considers the last input only
28 // we make it consider the first input only 38 // we make it consider the first input only
@@ -78,29 +88,26 @@ function getMaxAudioBitrate (type: 'aac' | 'mp3' | string, bitrate: number) {
78 } 88 }
79} 89}
80 90
81async function getVideoStreamSize (path: string, existingProbe?: FfprobeData): Promise<{ width: number, height: number }> { 91// ---------------------------------------------------------------------------
82 const videoStream = await getVideoStreamFromFile(path, existingProbe) 92// Video
83 93// ---------------------------------------------------------------------------
84 return videoStream === null
85 ? { width: 0, height: 0 }
86 : { width: videoStream.width, height: videoStream.height }
87}
88 94
89async function getVideoFileResolution (path: string, existingProbe?: FfprobeData) { 95async function getVideoStreamDimensionsInfo (path: string, existingProbe?: FfprobeData) {
90 const size = await getVideoStreamSize(path, existingProbe) 96 const videoStream = await getVideoStream(path, existingProbe)
97 if (!videoStream) return undefined
91 98
92 return { 99 return {
93 width: size.width, 100 width: videoStream.width,
94 height: size.height, 101 height: videoStream.height,
95 ratio: Math.max(size.height, size.width) / Math.min(size.height, size.width), 102 ratio: Math.max(videoStream.height, videoStream.width) / Math.min(videoStream.height, videoStream.width),
96 resolution: Math.min(size.height, size.width), 103 resolution: Math.min(videoStream.height, videoStream.width),
97 isPortraitMode: size.height > size.width 104 isPortraitMode: videoStream.height > videoStream.width
98 } 105 }
99} 106}
100 107
101async function getVideoFileFPS (path: string, existingProbe?: FfprobeData) { 108async function getVideoStreamFPS (path: string, existingProbe?: FfprobeData) {
102 const videoStream = await getVideoStreamFromFile(path, existingProbe) 109 const videoStream = await getVideoStream(path, existingProbe)
103 if (videoStream === null) return 0 110 if (!videoStream) return 0
104 111
105 for (const key of [ 'avg_frame_rate', 'r_frame_rate' ]) { 112 for (const key of [ 'avg_frame_rate', 'r_frame_rate' ]) {
106 const valuesText: string = videoStream[key] 113 const valuesText: string = videoStream[key]
@@ -116,19 +123,19 @@ async function getVideoFileFPS (path: string, existingProbe?: FfprobeData) {
116 return 0 123 return 0
117} 124}
118 125
119async function getMetadataFromFile (path: string, existingProbe?: FfprobeData) { 126async function buildFileMetadata (path: string, existingProbe?: FfprobeData) {
120 const metadata = existingProbe || await ffprobePromise(path) 127 const metadata = existingProbe || await ffprobePromise(path)
121 128
122 return new VideoFileMetadata(metadata) 129 return new VideoFileMetadata(metadata)
123} 130}
124 131
125async function getVideoFileBitrate (path: string, existingProbe?: FfprobeData): Promise<number> { 132async function getVideoStreamBitrate (path: string, existingProbe?: FfprobeData): Promise<number> {
126 const metadata = await getMetadataFromFile(path, existingProbe) 133 const metadata = await buildFileMetadata(path, existingProbe)
127 134
128 let bitrate = metadata.format.bit_rate as number 135 let bitrate = metadata.format.bit_rate as number
129 if (bitrate && !isNaN(bitrate)) return bitrate 136 if (bitrate && !isNaN(bitrate)) return bitrate
130 137
131 const videoStream = await getVideoStreamFromFile(path, existingProbe) 138 const videoStream = await getVideoStream(path, existingProbe)
132 if (!videoStream) return undefined 139 if (!videoStream) return undefined
133 140
134 bitrate = videoStream?.bit_rate 141 bitrate = videoStream?.bit_rate
@@ -137,51 +144,30 @@ async function getVideoFileBitrate (path: string, existingProbe?: FfprobeData):
137 return undefined 144 return undefined
138} 145}
139 146
140async function getDurationFromVideoFile (path: string, existingProbe?: FfprobeData) { 147async function getVideoStreamDuration (path: string, existingProbe?: FfprobeData) {
141 const metadata = await getMetadataFromFile(path, existingProbe) 148 const metadata = await buildFileMetadata(path, existingProbe)
142 149
143 return Math.round(metadata.format.duration) 150 return Math.round(metadata.format.duration)
144} 151}
145 152
146async function getVideoStreamFromFile (path: string, existingProbe?: FfprobeData) { 153async function getVideoStream (path: string, existingProbe?: FfprobeData) {
147 const metadata = await getMetadataFromFile(path, existingProbe) 154 const metadata = await buildFileMetadata(path, existingProbe)
148
149 return metadata.streams.find(s => s.codec_type === 'video') || null
150}
151
152async function canDoQuickAudioTranscode (path: string, probe?: FfprobeData): Promise<boolean> {
153 const parsedAudio = await getAudioStream(path, probe)
154
155 if (!parsedAudio.audioStream) return true
156
157 if (parsedAudio.audioStream['codec_name'] !== 'aac') return false
158
159 const audioBitrate = parsedAudio.bitrate
160 if (!audioBitrate) return false
161
162 const maxAudioBitrate = getMaxAudioBitrate('aac', audioBitrate)
163 if (maxAudioBitrate !== -1 && audioBitrate > maxAudioBitrate) return false
164
165 const channelLayout = parsedAudio.audioStream['channel_layout']
166 // Causes playback issues with Chrome
167 if (!channelLayout || channelLayout === 'unknown') return false
168 155
169 return true 156 return metadata.streams.find(s => s.codec_type === 'video')
170} 157}
171 158
172// --------------------------------------------------------------------------- 159// ---------------------------------------------------------------------------
173 160
174export { 161export {
175 getVideoStreamSize, 162 getVideoStreamDimensionsInfo,
176 getVideoFileResolution, 163 buildFileMetadata,
177 getMetadataFromFile,
178 getMaxAudioBitrate, 164 getMaxAudioBitrate,
179 getVideoStreamFromFile, 165 getVideoStream,
180 getDurationFromVideoFile, 166 getVideoStreamDuration,
181 getAudioStream, 167 getAudioStream,
182 getVideoFileFPS, 168 getVideoStreamFPS,
183 isAudioFile, 169 isAudioFile,
184 ffprobePromise, 170 ffprobePromise,
185 getVideoFileBitrate, 171 getVideoStreamBitrate,
186 canDoQuickAudioTranscode 172 hasAudioStream
187} 173}
diff --git a/shared/models/server/custom-config.model.ts b/shared/models/server/custom-config.model.ts
index 52d3d9588..c9e7654de 100644
--- a/shared/models/server/custom-config.model.ts
+++ b/shared/models/server/custom-config.model.ts
@@ -143,6 +143,10 @@ export interface CustomConfig {
143 } 143 }
144 } 144 }
145 145
146 videoEditor: {
147 enabled: boolean
148 }
149
146 import: { 150 import: {
147 videos: { 151 videos: {
148 concurrency: number 152 concurrency: number
diff --git a/shared/models/server/job.model.ts b/shared/models/server/job.model.ts
index 1519d1c3e..d0293f542 100644
--- a/shared/models/server/job.model.ts
+++ b/shared/models/server/job.model.ts
@@ -1,4 +1,5 @@
1import { ContextType } from '../activitypub/context' 1import { ContextType } from '../activitypub/context'
2import { VideoEditorTaskCut } from '../videos/editor'
2import { VideoResolution } from '../videos/file/video-resolution.enum' 3import { VideoResolution } from '../videos/file/video-resolution.enum'
3import { SendEmailOptions } from './emailer.model' 4import { SendEmailOptions } from './emailer.model'
4 5
@@ -20,6 +21,7 @@ export type JobType =
20 | 'video-live-ending' 21 | 'video-live-ending'
21 | 'actor-keys' 22 | 'actor-keys'
22 | 'move-to-object-storage' 23 | 'move-to-object-storage'
24 | 'video-edition'
23 25
24export interface Job { 26export interface Job {
25 id: number 27 id: number
@@ -155,3 +157,40 @@ export interface MoveObjectStoragePayload {
155 videoUUID: string 157 videoUUID: string
156 isNewVideo: boolean 158 isNewVideo: boolean
157} 159}
160
161export type VideoEditorTaskCutPayload = VideoEditorTaskCut
162
163export type VideoEditorTaskIntroPayload = {
164 name: 'add-intro'
165
166 options: {
167 file: string
168 }
169}
170
171export type VideoEditorTaskOutroPayload = {
172 name: 'add-outro'
173
174 options: {
175 file: string
176 }
177}
178
179export type VideoEditorTaskWatermarkPayload = {
180 name: 'add-watermark'
181
182 options: {
183 file: string
184 }
185}
186
187export type VideoEditionTaskPayload =
188 VideoEditorTaskCutPayload |
189 VideoEditorTaskIntroPayload |
190 VideoEditorTaskOutroPayload |
191 VideoEditorTaskWatermarkPayload
192
193export interface VideoEditionPayload {
194 videoUUID: string
195 tasks: VideoEditionTaskPayload[]
196}
diff --git a/shared/models/server/server-config.model.ts b/shared/models/server/server-config.model.ts
index 32be96b9d..0fe8b0de8 100644
--- a/shared/models/server/server-config.model.ts
+++ b/shared/models/server/server-config.model.ts
@@ -167,6 +167,10 @@ export interface ServerConfig {
167 } 167 }
168 } 168 }
169 169
170 videoEditor: {
171 enabled: boolean
172 }
173
170 import: { 174 import: {
171 videos: { 175 videos: {
172 http: { 176 http: {
diff --git a/shared/models/videos/editor/index.ts b/shared/models/videos/editor/index.ts
new file mode 100644
index 000000000..3436f2c3f
--- /dev/null
+++ b/shared/models/videos/editor/index.ts
@@ -0,0 +1 @@
export * from './video-editor-create-edit.model'
diff --git a/shared/models/videos/editor/video-editor-create-edit.model.ts b/shared/models/videos/editor/video-editor-create-edit.model.ts
new file mode 100644
index 000000000..36b7c8d55
--- /dev/null
+++ b/shared/models/videos/editor/video-editor-create-edit.model.ts
@@ -0,0 +1,42 @@
1export interface VideoEditorCreateEdition {
2 tasks: VideoEditorTask[]
3}
4
5export type VideoEditorTask =
6 VideoEditorTaskCut |
7 VideoEditorTaskIntro |
8 VideoEditorTaskOutro |
9 VideoEditorTaskWatermark
10
11export interface VideoEditorTaskCut {
12 name: 'cut'
13
14 options: {
15 start?: number
16 end?: number
17 }
18}
19
20export interface VideoEditorTaskIntro {
21 name: 'add-intro'
22
23 options: {
24 file: Blob | string
25 }
26}
27
28export interface VideoEditorTaskOutro {
29 name: 'add-outro'
30
31 options: {
32 file: Blob | string
33 }
34}
35
36export interface VideoEditorTaskWatermark {
37 name: 'add-watermark'
38
39 options: {
40 file: Blob | string
41 }
42}
diff --git a/shared/models/videos/index.ts b/shared/models/videos/index.ts
index 67614efc9..e8eb227ab 100644
--- a/shared/models/videos/index.ts
+++ b/shared/models/videos/index.ts
@@ -3,6 +3,7 @@ export * from './caption'
3export * from './change-ownership' 3export * from './change-ownership'
4export * from './channel' 4export * from './channel'
5export * from './comment' 5export * from './comment'
6export * from './editor'
6export * from './live' 7export * from './live'
7export * from './file' 8export * from './file'
8export * from './import' 9export * from './import'
diff --git a/shared/models/videos/transcoding/video-transcoding-fps.model.ts b/shared/models/videos/transcoding/video-transcoding-fps.model.ts
index 25fc1c2da..9a330ac94 100644
--- a/shared/models/videos/transcoding/video-transcoding-fps.model.ts
+++ b/shared/models/videos/transcoding/video-transcoding-fps.model.ts
@@ -2,6 +2,7 @@ export type VideoTranscodingFPS = {
2 MIN: number 2 MIN: number
3 STANDARD: number[] 3 STANDARD: number[]
4 HD_STANDARD: number[] 4 HD_STANDARD: number[]
5 AUDIO_MERGE: number
5 AVERAGE: number 6 AVERAGE: number
6 MAX: number 7 MAX: number
7 KEEP_ORIGIN_FPS_RESOLUTION_MIN: number 8 KEEP_ORIGIN_FPS_RESOLUTION_MIN: number
diff --git a/shared/models/videos/transcoding/video-transcoding.model.ts b/shared/models/videos/transcoding/video-transcoding.model.ts
index 3a7fb6472..91eacf8dc 100644
--- a/shared/models/videos/transcoding/video-transcoding.model.ts
+++ b/shared/models/videos/transcoding/video-transcoding.model.ts
@@ -7,8 +7,11 @@ export type EncoderOptionsBuilderParams = {
7 7
8 resolution: VideoResolution 8 resolution: VideoResolution
9 9
10 // Could be null for "merge audio" transcoding 10 // If PeerTube applies a filter, transcoding profile must not copy input stream
11 fps?: number 11 canCopyAudio: boolean
12 canCopyVideo: boolean
13
14 fps: number
12 15
13 // Could be undefined if we could not get input bitrate (some RTMP streams for example) 16 // Could be undefined if we could not get input bitrate (some RTMP streams for example)
14 inputBitrate: number 17 inputBitrate: number
diff --git a/shared/models/videos/video-state.enum.ts b/shared/models/videos/video-state.enum.ts
index 09268d2ff..e45e4adc2 100644
--- a/shared/models/videos/video-state.enum.ts
+++ b/shared/models/videos/video-state.enum.ts
@@ -6,5 +6,6 @@ export const enum VideoState {
6 LIVE_ENDED = 5, 6 LIVE_ENDED = 5,
7 TO_MOVE_TO_EXTERNAL_STORAGE = 6, 7 TO_MOVE_TO_EXTERNAL_STORAGE = 6,
8 TRANSCODING_FAILED = 7, 8 TRANSCODING_FAILED = 7,
9 TO_MOVE_TO_EXTERNAL_STORAGE_FAILED = 8 9 TO_MOVE_TO_EXTERNAL_STORAGE_FAILED = 8,
10 TO_EDIT = 9
10} 11}
diff --git a/shared/server-commands/server/config-command.ts b/shared/server-commands/server/config-command.ts
index 797231b1d..c0042060b 100644
--- a/shared/server-commands/server/config-command.ts
+++ b/shared/server-commands/server/config-command.ts
@@ -59,6 +59,9 @@ export class ConfigCommand extends AbstractCommand {
59 newConfig: { 59 newConfig: {
60 transcoding: { 60 transcoding: {
61 enabled: false 61 enabled: false
62 },
63 videoEditor: {
64 enabled: false
62 } 65 }
63 } 66 }
64 }) 67 })
@@ -69,6 +72,10 @@ export class ConfigCommand extends AbstractCommand {
69 newConfig: { 72 newConfig: {
70 transcoding: { 73 transcoding: {
71 enabled: true, 74 enabled: true,
75
76 allowAudioFiles: true,
77 allowAdditionalExtensions: true,
78
72 resolutions: ConfigCommand.getCustomConfigResolutions(true), 79 resolutions: ConfigCommand.getCustomConfigResolutions(true),
73 80
74 webtorrent: { 81 webtorrent: {
@@ -82,6 +89,28 @@ export class ConfigCommand extends AbstractCommand {
82 }) 89 })
83 } 90 }
84 91
92 enableMinimumTranscoding (webtorrent = true, hls = true) {
93 return this.updateExistingSubConfig({
94 newConfig: {
95 transcoding: {
96 enabled: true,
97 resolutions: {
98 ...ConfigCommand.getCustomConfigResolutions(false),
99
100 '240p': true
101 },
102
103 webtorrent: {
104 enabled: webtorrent
105 },
106 hls: {
107 enabled: hls
108 }
109 }
110 }
111 })
112 }
113
85 getConfig (options: OverrideCommandOptions = {}) { 114 getConfig (options: OverrideCommandOptions = {}) {
86 const path = '/api/v1/config' 115 const path = '/api/v1/config'
87 116
@@ -148,7 +177,7 @@ export class ConfigCommand extends AbstractCommand {
148 async updateExistingSubConfig (options: OverrideCommandOptions & { 177 async updateExistingSubConfig (options: OverrideCommandOptions & {
149 newConfig: DeepPartial<CustomConfig> 178 newConfig: DeepPartial<CustomConfig>
150 }) { 179 }) {
151 const existing = await this.getCustomConfig(options) 180 const existing = await this.getCustomConfig({ ...options, expectedStatus: HttpStatusCode.OK_200 })
152 181
153 return this.updateCustomConfig({ ...options, newCustomConfig: merge({}, existing, options.newConfig) }) 182 return this.updateCustomConfig({ ...options, newCustomConfig: merge({}, existing, options.newConfig) })
154 } 183 }
@@ -282,6 +311,9 @@ export class ConfigCommand extends AbstractCommand {
282 } 311 }
283 } 312 }
284 }, 313 },
314 videoEditor: {
315 enabled: false
316 },
285 import: { 317 import: {
286 videos: { 318 videos: {
287 concurrency: 3, 319 concurrency: 3,
diff --git a/shared/server-commands/server/server.ts b/shared/server-commands/server/server.ts
index da89fd876..af4423e8d 100644
--- a/shared/server-commands/server/server.ts
+++ b/shared/server-commands/server/server.ts
@@ -25,6 +25,7 @@ import {
25 PlaylistsCommand, 25 PlaylistsCommand,
26 ServicesCommand, 26 ServicesCommand,
27 StreamingPlaylistsCommand, 27 StreamingPlaylistsCommand,
28 VideoEditorCommand,
28 VideosCommand 29 VideosCommand
29} from '../videos' 30} from '../videos'
30import { CommentsCommand } from '../videos/comments-command' 31import { CommentsCommand } from '../videos/comments-command'
@@ -124,6 +125,7 @@ export class PeerTubeServer {
124 login?: LoginCommand 125 login?: LoginCommand
125 users?: UsersCommand 126 users?: UsersCommand
126 objectStorage?: ObjectStorageCommand 127 objectStorage?: ObjectStorageCommand
128 videoEditor?: VideoEditorCommand
127 videos?: VideosCommand 129 videos?: VideosCommand
128 130
129 constructor (options: { serverNumber: number } | { url: string }) { 131 constructor (options: { serverNumber: number } | { url: string }) {
@@ -394,5 +396,6 @@ export class PeerTubeServer {
394 this.users = new UsersCommand(this) 396 this.users = new UsersCommand(this)
395 this.videos = new VideosCommand(this) 397 this.videos = new VideosCommand(this)
396 this.objectStorage = new ObjectStorageCommand(this) 398 this.objectStorage = new ObjectStorageCommand(this)
399 this.videoEditor = new VideoEditorCommand(this)
397 } 400 }
398} 401}
diff --git a/shared/server-commands/videos/index.ts b/shared/server-commands/videos/index.ts
index 68a188b21..154aed9a6 100644
--- a/shared/server-commands/videos/index.ts
+++ b/shared/server-commands/videos/index.ts
@@ -12,4 +12,5 @@ export * from './playlists-command'
12export * from './services-command' 12export * from './services-command'
13export * from './streaming-playlists-command' 13export * from './streaming-playlists-command'
14export * from './comments-command' 14export * from './comments-command'
15export * from './video-editor-command'
15export * from './videos-command' 16export * from './videos-command'
diff --git a/shared/server-commands/videos/video-editor-command.ts b/shared/server-commands/videos/video-editor-command.ts
new file mode 100644
index 000000000..485edce8e
--- /dev/null
+++ b/shared/server-commands/videos/video-editor-command.ts
@@ -0,0 +1,67 @@
1import { HttpStatusCode, VideoEditorTask } from '@shared/models'
2import { AbstractCommand, OverrideCommandOptions } from '../shared'
3
4export class VideoEditorCommand extends AbstractCommand {
5
6 static getComplexTask (): VideoEditorTask[] {
7 return [
8 // Total duration: 2
9 {
10 name: 'cut',
11 options: {
12 start: 1,
13 end: 3
14 }
15 },
16
17 // Total duration: 7
18 {
19 name: 'add-outro',
20 options: {
21 file: 'video_short.webm'
22 }
23 },
24
25 {
26 name: 'add-watermark',
27 options: {
28 file: 'thumbnail.png'
29 }
30 },
31
32 // Total duration: 9
33 {
34 name: 'add-intro',
35 options: {
36 file: 'video_very_short_240p.mp4'
37 }
38 }
39 ]
40 }
41
42 createEditionTasks (options: OverrideCommandOptions & {
43 videoId: number | string
44 tasks: VideoEditorTask[]
45 }) {
46 const path = '/api/v1/videos/' + options.videoId + '/editor/edit'
47 const attaches: { [id: string]: any } = {}
48
49 for (let i = 0; i < options.tasks.length; i++) {
50 const task = options.tasks[i]
51
52 if (task.name === 'add-intro' || task.name === 'add-outro' || task.name === 'add-watermark') {
53 attaches[`tasks[${i}][options][file]`] = task.options.file
54 }
55 }
56
57 return this.postUploadRequest({
58 ...options,
59
60 path,
61 attaches,
62 fields: { tasks: options.tasks },
63 implicitToken: true,
64 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
65 })
66 }
67}