2
1

video-format-utils.ts 11 KB

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