Add hls support on server
[oweals/peertube.git] / server / models / video / video-format-utils.ts
1 import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos'
2 import { VideoModel } from './video'
3 import { VideoFileModel } from './video-file'
4 import {
5   ActivityPlaylistInfohashesObject,
6   ActivityPlaylistSegmentHashesObject,
7   ActivityUrlObject,
8   VideoTorrentObject
9 } from '../../../shared/models/activitypub/objects'
10 import { CONFIG, MIMETYPES, THUMBNAILS_SIZE } from '../../initializers'
11 import { VideoCaptionModel } from './video-caption'
12 import {
13   getVideoCommentsActivityPubUrl,
14   getVideoDislikesActivityPubUrl,
15   getVideoLikesActivityPubUrl,
16   getVideoSharesActivityPubUrl
17 } from '../../lib/activitypub'
18 import { isArray } from '../../helpers/custom-validators/misc'
19 import { VideoStreamingPlaylist } from '../../../shared/models/videos/video-streaming-playlist.model'
20 import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
21
22 export type VideoFormattingJSONOptions = {
23   completeDescription?: boolean
24   additionalAttributes: {
25     state?: boolean,
26     waitTranscoding?: boolean,
27     scheduledUpdate?: boolean,
28     blacklistInfo?: boolean
29   }
30 }
31 function videoModelToFormattedJSON (video: VideoModel, options?: VideoFormattingJSONOptions): Video {
32   const formattedAccount = video.VideoChannel.Account.toFormattedJSON()
33   const formattedVideoChannel = video.VideoChannel.toFormattedJSON()
34
35   const userHistory = isArray(video.UserVideoHistories) ? video.UserVideoHistories[0] : undefined
36
37   const videoObject: Video = {
38     id: video.id,
39     uuid: video.uuid,
40     name: video.name,
41     category: {
42       id: video.category,
43       label: VideoModel.getCategoryLabel(video.category)
44     },
45     licence: {
46       id: video.licence,
47       label: VideoModel.getLicenceLabel(video.licence)
48     },
49     language: {
50       id: video.language,
51       label: VideoModel.getLanguageLabel(video.language)
52     },
53     privacy: {
54       id: video.privacy,
55       label: VideoModel.getPrivacyLabel(video.privacy)
56     },
57     nsfw: video.nsfw,
58     description: options && options.completeDescription === true ? video.description : video.getTruncatedDescription(),
59     isLocal: video.isOwned(),
60     duration: video.duration,
61     views: video.views,
62     likes: video.likes,
63     dislikes: video.dislikes,
64     thumbnailPath: video.getThumbnailStaticPath(),
65     previewPath: video.getPreviewStaticPath(),
66     embedPath: video.getEmbedStaticPath(),
67     createdAt: video.createdAt,
68     updatedAt: video.updatedAt,
69     publishedAt: video.publishedAt,
70     account: {
71       id: formattedAccount.id,
72       uuid: formattedAccount.uuid,
73       name: formattedAccount.name,
74       displayName: formattedAccount.displayName,
75       url: formattedAccount.url,
76       host: formattedAccount.host,
77       avatar: formattedAccount.avatar
78     },
79     channel: {
80       id: formattedVideoChannel.id,
81       uuid: formattedVideoChannel.uuid,
82       name: formattedVideoChannel.name,
83       displayName: formattedVideoChannel.displayName,
84       url: formattedVideoChannel.url,
85       host: formattedVideoChannel.host,
86       avatar: formattedVideoChannel.avatar
87     },
88
89     userHistory: userHistory ? {
90       currentTime: userHistory.currentTime
91     } : undefined
92   }
93
94   if (options) {
95     if (options.additionalAttributes.state === true) {
96       videoObject.state = {
97         id: video.state,
98         label: VideoModel.getStateLabel(video.state)
99       }
100     }
101
102     if (options.additionalAttributes.waitTranscoding === true) {
103       videoObject.waitTranscoding = video.waitTranscoding
104     }
105
106     if (options.additionalAttributes.scheduledUpdate === true && video.ScheduleVideoUpdate) {
107       videoObject.scheduledUpdate = {
108         updateAt: video.ScheduleVideoUpdate.updateAt,
109         privacy: video.ScheduleVideoUpdate.privacy || undefined
110       }
111     }
112
113     if (options.additionalAttributes.blacklistInfo === true) {
114       videoObject.blacklisted = !!video.VideoBlacklist
115       videoObject.blacklistedReason = video.VideoBlacklist ? video.VideoBlacklist.reason : null
116     }
117   }
118
119   return videoObject
120 }
121
122 function videoModelToFormattedDetailsJSON (video: VideoModel): VideoDetails {
123   const formattedJson = video.toFormattedJSON({
124     additionalAttributes: {
125       scheduledUpdate: true,
126       blacklistInfo: true
127     }
128   })
129
130   const { baseUrlHttp, baseUrlWs } = video.getBaseUrls()
131
132   const tags = video.Tags ? video.Tags.map(t => t.name) : []
133
134   const streamingPlaylists = streamingPlaylistsModelToFormattedJSON(video, video.VideoStreamingPlaylists)
135
136   const detailsJson = {
137     support: video.support,
138     descriptionPath: video.getDescriptionAPIPath(),
139     channel: video.VideoChannel.toFormattedJSON(),
140     account: video.VideoChannel.Account.toFormattedJSON(),
141     tags,
142     commentsEnabled: video.commentsEnabled,
143     waitTranscoding: video.waitTranscoding,
144     state: {
145       id: video.state,
146       label: VideoModel.getStateLabel(video.state)
147     },
148
149     trackerUrls: video.getTrackerUrls(baseUrlHttp, baseUrlWs),
150
151     files: [],
152     streamingPlaylists
153   }
154
155   // Format and sort video files
156   detailsJson.files = videoFilesModelToFormattedJSON(video, video.VideoFiles)
157
158   return Object.assign(formattedJson, detailsJson)
159 }
160
161 function streamingPlaylistsModelToFormattedJSON (video: VideoModel, playlists: VideoStreamingPlaylistModel[]): VideoStreamingPlaylist[] {
162   if (isArray(playlists) === false) return []
163
164   return playlists
165     .map(playlist => {
166       const redundancies = isArray(playlist.RedundancyVideos)
167         ? playlist.RedundancyVideos.map(r => ({ baseUrl: r.fileUrl }))
168         : []
169
170       return {
171         id: playlist.id,
172         type: playlist.type,
173         playlistUrl: playlist.playlistUrl,
174         segmentsSha256Url: playlist.segmentsSha256Url,
175         redundancies
176       } as VideoStreamingPlaylist
177     })
178 }
179
180 function videoFilesModelToFormattedJSON (video: VideoModel, videoFiles: VideoFileModel[]): VideoFile[] {
181   const { baseUrlHttp, baseUrlWs } = video.getBaseUrls()
182
183   return videoFiles
184     .map(videoFile => {
185       let resolutionLabel = videoFile.resolution + 'p'
186
187       return {
188         resolution: {
189           id: videoFile.resolution,
190           label: resolutionLabel
191         },
192         magnetUri: video.generateMagnetUri(videoFile, baseUrlHttp, baseUrlWs),
193         size: videoFile.size,
194         fps: videoFile.fps,
195         torrentUrl: video.getTorrentUrl(videoFile, baseUrlHttp),
196         torrentDownloadUrl: video.getTorrentDownloadUrl(videoFile, baseUrlHttp),
197         fileUrl: video.getVideoFileUrl(videoFile, baseUrlHttp),
198         fileDownloadUrl: video.getVideoFileDownloadUrl(videoFile, baseUrlHttp)
199       } as VideoFile
200     })
201     .sort((a, b) => {
202       if (a.resolution.id < b.resolution.id) return 1
203       if (a.resolution.id === b.resolution.id) return 0
204       return -1
205     })
206 }
207
208 function videoModelToActivityPubObject (video: VideoModel): VideoTorrentObject {
209   const { baseUrlHttp, baseUrlWs } = video.getBaseUrls()
210   if (!video.Tags) video.Tags = []
211
212   const tag = video.Tags.map(t => ({
213     type: 'Hashtag' as 'Hashtag',
214     name: t.name
215   }))
216
217   let language
218   if (video.language) {
219     language = {
220       identifier: video.language,
221       name: VideoModel.getLanguageLabel(video.language)
222     }
223   }
224
225   let category
226   if (video.category) {
227     category = {
228       identifier: video.category + '',
229       name: VideoModel.getCategoryLabel(video.category)
230     }
231   }
232
233   let licence
234   if (video.licence) {
235     licence = {
236       identifier: video.licence + '',
237       name: VideoModel.getLicenceLabel(video.licence)
238     }
239   }
240
241   const url: ActivityUrlObject[] = []
242   for (const file of video.VideoFiles) {
243     url.push({
244       type: 'Link',
245       mimeType: MIMETYPES.VIDEO.EXT_MIMETYPE[ file.extname ] as any,
246       mediaType: MIMETYPES.VIDEO.EXT_MIMETYPE[ file.extname ] as any,
247       href: video.getVideoFileUrl(file, baseUrlHttp),
248       height: file.resolution,
249       size: file.size,
250       fps: file.fps
251     })
252
253     url.push({
254       type: 'Link',
255       mimeType: 'application/x-bittorrent' as 'application/x-bittorrent',
256       mediaType: 'application/x-bittorrent' as 'application/x-bittorrent',
257       href: video.getTorrentUrl(file, baseUrlHttp),
258       height: file.resolution
259     })
260
261     url.push({
262       type: 'Link',
263       mimeType: 'application/x-bittorrent;x-scheme-handler/magnet' as 'application/x-bittorrent;x-scheme-handler/magnet',
264       mediaType: 'application/x-bittorrent;x-scheme-handler/magnet' as 'application/x-bittorrent;x-scheme-handler/magnet',
265       href: video.generateMagnetUri(file, baseUrlHttp, baseUrlWs),
266       height: file.resolution
267     })
268   }
269
270   for (const playlist of (video.VideoStreamingPlaylists || [])) {
271     let tag: (ActivityPlaylistSegmentHashesObject | ActivityPlaylistInfohashesObject)[]
272
273     tag = playlist.p2pMediaLoaderInfohashes
274                   .map(i => ({ type: 'Infohash' as 'Infohash', name: i }))
275     tag.push({
276       type: 'Link',
277       name: 'sha256',
278       mimeType: 'application/json' as 'application/json',
279       mediaType: 'application/json' as 'application/json',
280       href: playlist.segmentsSha256Url
281     })
282
283     url.push({
284       type: 'Link',
285       mimeType: 'application/x-mpegURL' as 'application/x-mpegURL',
286       mediaType: 'application/x-mpegURL' as 'application/x-mpegURL',
287       href: playlist.playlistUrl,
288       tag
289     })
290   }
291
292   // Add video url too
293   url.push({
294     type: 'Link',
295     mimeType: 'text/html',
296     mediaType: 'text/html',
297     href: CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid
298   })
299
300   const subtitleLanguage = []
301   for (const caption of video.VideoCaptions) {
302     subtitleLanguage.push({
303       identifier: caption.language,
304       name: VideoCaptionModel.getLanguageLabel(caption.language)
305     })
306   }
307
308   return {
309     type: 'Video' as 'Video',
310     id: video.url,
311     name: video.name,
312     duration: getActivityStreamDuration(video.duration),
313     uuid: video.uuid,
314     tag,
315     category,
316     licence,
317     language,
318     views: video.views,
319     sensitive: video.nsfw,
320     waitTranscoding: video.waitTranscoding,
321     state: video.state,
322     commentsEnabled: video.commentsEnabled,
323     published: video.publishedAt.toISOString(),
324     updated: video.updatedAt.toISOString(),
325     mediaType: 'text/markdown',
326     content: video.getTruncatedDescription(),
327     support: video.support,
328     subtitleLanguage,
329     icon: {
330       type: 'Image',
331       url: video.getThumbnailUrl(baseUrlHttp),
332       mediaType: 'image/jpeg',
333       width: THUMBNAILS_SIZE.width,
334       height: THUMBNAILS_SIZE.height
335     },
336     url,
337     likes: getVideoLikesActivityPubUrl(video),
338     dislikes: getVideoDislikesActivityPubUrl(video),
339     shares: getVideoSharesActivityPubUrl(video),
340     comments: getVideoCommentsActivityPubUrl(video),
341     attributedTo: [
342       {
343         type: 'Person',
344         id: video.VideoChannel.Account.Actor.url
345       },
346       {
347         type: 'Group',
348         id: video.VideoChannel.Actor.url
349       }
350     ]
351   }
352 }
353
354 function getActivityStreamDuration (duration: number) {
355   // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration
356   return 'PT' + duration + 'S'
357 }
358
359 export {
360   videoModelToFormattedJSON,
361   videoModelToFormattedDetailsJSON,
362   videoFilesModelToFormattedJSON,
363   videoModelToActivityPubObject,
364   getActivityStreamDuration
365 }