video-format-utils.ts 11 KB

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