aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--packages/tests/src/feeds/feeds.ts32
-rw-r--r--server/server/controllers/feeds/video-feeds.ts7
-rw-r--r--server/server/controllers/feeds/video-podcast-feeds.ts8
-rw-r--r--server/server/initializers/constants.ts3
-rw-r--r--server/server/lib/video-file.ts10
-rw-r--r--server/server/models/redundancy/video-redundancy.ts14
-rw-r--r--server/server/models/video/formatter/video-activity-pub-format.ts12
7 files changed, 58 insertions, 28 deletions
diff --git a/packages/tests/src/feeds/feeds.ts b/packages/tests/src/feeds/feeds.ts
index 7587bb34e..ed833ffd1 100644
--- a/packages/tests/src/feeds/feeds.ts
+++ b/packages/tests/src/feeds/feeds.ts
@@ -46,13 +46,7 @@ describe('Test syndication feeds', () => {
46 46
47 // Run servers 47 // Run servers
48 servers = await createMultipleServers(2) 48 servers = await createMultipleServers(2)
49 serverHLSOnly = await createSingleServer(3, { 49 serverHLSOnly = await createSingleServer(3)
50 transcoding: {
51 enabled: true,
52 web_videos: { enabled: false },
53 hls: { enabled: true }
54 }
55 })
56 50
57 await setAccessTokensToServers([ ...servers, serverHLSOnly ]) 51 await setAccessTokensToServers([ ...servers, serverHLSOnly ])
58 await setDefaultChannelAvatar(servers[0]) 52 await setDefaultChannelAvatar(servers[0])
@@ -60,6 +54,7 @@ describe('Test syndication feeds', () => {
60 await doubleFollow(servers[0], servers[1]) 54 await doubleFollow(servers[0], servers[1])
61 55
62 await servers[0].config.enableLive({ allowReplay: false, transcoding: false }) 56 await servers[0].config.enableLive({ allowReplay: false, transcoding: false })
57 await serverHLSOnly.config.enableTranscoding({ webVideo: false, hls: true, with0p: true })
63 58
64 { 59 {
65 const user = await servers[0].users.getMyInfo() 60 const user = await servers[0].users.getMyInfo()
@@ -397,9 +392,9 @@ describe('Test syndication feeds', () => {
397 const jsonObj = JSON.parse(json) 392 const jsonObj = JSON.parse(json)
398 expect(jsonObj.items.length).to.be.equal(1) 393 expect(jsonObj.items.length).to.be.equal(1)
399 expect(jsonObj.items[0].attachments).to.exist 394 expect(jsonObj.items[0].attachments).to.exist
400 expect(jsonObj.items[0].attachments.length).to.be.eq(4) 395 expect(jsonObj.items[0].attachments.length).to.be.eq(6)
401 396
402 for (let i = 0; i < 4; i++) { 397 for (let i = 0; i < 6; i++) {
403 expect(jsonObj.items[0].attachments[i].mime_type).to.be.eq('application/x-bittorrent') 398 expect(jsonObj.items[0].attachments[i].mime_type).to.be.eq('application/x-bittorrent')
404 expect(jsonObj.items[0].attachments[i].size_in_bytes).to.be.greaterThan(0) 399 expect(jsonObj.items[0].attachments[i].size_in_bytes).to.be.greaterThan(0)
405 expect(jsonObj.items[0].attachments[i].url).to.exist 400 expect(jsonObj.items[0].attachments[i].url).to.exist
@@ -450,6 +445,25 @@ describe('Test syndication feeds', () => {
450 await makeRawRequest({ url: imageUrl, expectedStatus: HttpStatusCode.OK_200 }) 445 await makeRawRequest({ url: imageUrl, expectedStatus: HttpStatusCode.OK_200 })
451 }) 446 })
452 }) 447 })
448
449 describe('XML feed', function () {
450
451 it('Should correctly have video mime types feed with HLS only', async function () {
452 this.timeout(120000)
453
454 const rss = await serverHLSOnly.feed.getXML({ feed: 'videos', ignoreCache: true })
455 const parser = new XMLParser({ parseAttributeValue: true, ignoreAttributes: false })
456 const xmlDoc = parser.parse(rss)
457
458 for (const media of xmlDoc.rss.channel.item['media:group']['media:content']) {
459 if (media['@_height'] === 0) {
460 expect(media['@_type']).to.equal('audio/mp4')
461 } else {
462 expect(media['@_type']).to.equal('video/mp4')
463 }
464 }
465 })
466 })
453 }) 467 })
454 468
455 describe('Video comments feed', function () { 469 describe('Video comments feed', function () {
diff --git a/server/server/controllers/feeds/video-feeds.ts b/server/server/controllers/feeds/video-feeds.ts
index fc0e6e0f7..679d73fff 100644
--- a/server/server/controllers/feeds/video-feeds.ts
+++ b/server/server/controllers/feeds/video-feeds.ts
@@ -3,9 +3,9 @@ import { extname } from 'path'
3import { Feed } from '@peertube/feed' 3import { Feed } from '@peertube/feed'
4import { cacheRouteFactory } from '@server/middlewares/index.js' 4import { cacheRouteFactory } from '@server/middlewares/index.js'
5import { VideoModel } from '@server/models/video/video.js' 5import { VideoModel } from '@server/models/video/video.js'
6import { VideoInclude } from '@peertube/peertube-models' 6import { VideoInclude, VideoResolution } from '@peertube/peertube-models'
7import { buildNSFWFilter } from '../../helpers/express-utils.js' 7import { buildNSFWFilter } from '../../helpers/express-utils.js'
8import { MIMETYPES, ROUTE_CACHE_LIFETIME, WEBSERVER } from '../../initializers/constants.js' 8import { ROUTE_CACHE_LIFETIME, WEBSERVER } from '../../initializers/constants.js'
9import { 9import {
10 asyncMiddleware, 10 asyncMiddleware,
11 commonVideosFiltersValidator, 11 commonVideosFiltersValidator,
@@ -17,6 +17,7 @@ import {
17 videoSubscriptionFeedsValidator 17 videoSubscriptionFeedsValidator
18} from '../../middlewares/index.js' 18} from '../../middlewares/index.js'
19import { buildFeedMetadata, getCommonVideoFeedAttributes, getVideosForFeeds, initFeed, sendFeed } from './shared/index.js' 19import { buildFeedMetadata, getCommonVideoFeedAttributes, getVideosForFeeds, initFeed, sendFeed } from './shared/index.js'
20import { getVideoFileMimeType } from '@server/lib/video-file.js'
20 21
21const videoFeedsRouter = express.Router() 22const videoFeedsRouter = express.Router()
22 23
@@ -137,7 +138,7 @@ function addVideosToFeed (feed: Feed, videos: VideoModel[]) {
137 138
138 const videoFiles = formattedVideoFiles.map(videoFile => { 139 const videoFiles = formattedVideoFiles.map(videoFile => {
139 return { 140 return {
140 type: MIMETYPES.VIDEO.EXT_MIMETYPE[extname(videoFile.fileUrl)], 141 type: getVideoFileMimeType(extname(videoFile.fileUrl), videoFile.resolution.id === VideoResolution.H_NOVIDEO),
141 medium: 'video', 142 medium: 'video',
142 height: videoFile.resolution.id, 143 height: videoFile.resolution.id,
143 fileSize: videoFile.size, 144 fileSize: videoFile.size,
diff --git a/server/server/controllers/feeds/video-podcast-feeds.ts b/server/server/controllers/feeds/video-podcast-feeds.ts
index 84d5acadd..ad694affa 100644
--- a/server/server/controllers/feeds/video-podcast-feeds.ts
+++ b/server/server/controllers/feeds/video-podcast-feeds.ts
@@ -15,6 +15,7 @@ import { asyncMiddleware, setFeedPodcastContentType, videoFeedsPodcastValidator
15import { VideoModel } from '../../models/video/video.js' 15import { VideoModel } from '../../models/video/video.js'
16import { VideoCaptionModel } from '../../models/video/video-caption.js' 16import { VideoCaptionModel } from '../../models/video/video-caption.js'
17import { buildFeedMetadata, getCommonVideoFeedAttributes, getVideosForFeeds, initFeed } from './shared/index.js' 17import { buildFeedMetadata, getCommonVideoFeedAttributes, getVideosForFeeds, initFeed } from './shared/index.js'
18import { getVideoFileMimeType } from '@server/lib/video-file.js'
18 19
19const videoPodcastFeedsRouter = express.Router() 20const videoPodcastFeedsRouter = express.Router()
20 21
@@ -243,11 +244,6 @@ async function addLivePodcastItem (options: {
243// --------------------------------------------------------------------------- 244// ---------------------------------------------------------------------------
244 245
245function buildVODWebVideoFile (video: MVideo, videoFile: VideoFile) { 246function buildVODWebVideoFile (video: MVideo, videoFile: VideoFile) {
246 const isAudio = videoFile.resolution.id === VideoResolution.H_NOVIDEO
247 const type = isAudio
248 ? MIMETYPES.AUDIO.EXT_MIMETYPE[extname(videoFile.fileUrl)]
249 : MIMETYPES.VIDEO.EXT_MIMETYPE[extname(videoFile.fileUrl)]
250
251 const sources = [ 247 const sources = [
252 { uri: videoFile.fileUrl }, 248 { uri: videoFile.fileUrl },
253 { uri: videoFile.torrentUrl, contentType: 'application/x-bittorrent' } 249 { uri: videoFile.torrentUrl, contentType: 'application/x-bittorrent' }
@@ -258,7 +254,7 @@ function buildVODWebVideoFile (video: MVideo, videoFile: VideoFile) {
258 } 254 }
259 255
260 return { 256 return {
261 type, 257 type: getVideoFileMimeType(extname(videoFile.fileUrl), videoFile.resolution.id === VideoResolution.H_NOVIDEO),
262 title: videoFile.resolution.label, 258 title: videoFile.resolution.label,
263 length: videoFile.size, 259 length: videoFile.size,
264 bitrate: videoFile.size / video.duration * 8, 260 bitrate: videoFile.size / video.duration * 8,
diff --git a/server/server/initializers/constants.ts b/server/server/initializers/constants.ts
index 38322aece..121433caf 100644
--- a/server/server/initializers/constants.ts
+++ b/server/server/initializers/constants.ts
@@ -630,9 +630,10 @@ const MIMETYPES = {
630 'audio/vnd.dlna.adts': '.aac', 630 'audio/vnd.dlna.adts': '.aac',
631 'audio/aac': '.aac', 631 'audio/aac': '.aac',
632 632
633 // Keep priority for preferred mime type
633 'audio/m4a': '.m4a', 634 'audio/m4a': '.m4a',
634 'audio/mp4': '.m4a',
635 'audio/x-m4a': '.m4a', 635 'audio/x-m4a': '.m4a',
636 'audio/mp4': '.m4a',
636 637
637 'audio/vnd.dolby.dd-raw': '.ac3', 638 'audio/vnd.dolby.dd-raw': '.ac3',
638 'audio/ac3': '.ac3' 639 'audio/ac3': '.ac3'
diff --git a/server/server/lib/video-file.ts b/server/server/lib/video-file.ts
index 9d8a6e8fc..ccc5668a2 100644
--- a/server/server/lib/video-file.ts
+++ b/server/server/lib/video-file.ts
@@ -8,6 +8,7 @@ import { ffprobePromise, getVideoStreamDimensionsInfo, getVideoStreamFPS, isAudi
8import { lTags } from './object-storage/shared/index.js' 8import { lTags } from './object-storage/shared/index.js'
9import { generateHLSVideoFilename, generateWebVideoFilename } from './paths.js' 9import { generateHLSVideoFilename, generateWebVideoFilename } from './paths.js'
10import { VideoPathManager } from './video-path-manager.js' 10import { VideoPathManager } from './video-path-manager.js'
11import { MIMETYPES } from '@server/initializers/constants.js'
11 12
12async function buildNewFile (options: { 13async function buildNewFile (options: {
13 path: string 14 path: string
@@ -130,6 +131,12 @@ async function buildFileMetadata (path: string, existingProbe?: FfprobeData) {
130 return new VideoFileMetadata(metadata) 131 return new VideoFileMetadata(metadata)
131} 132}
132 133
134function getVideoFileMimeType (extname: string, isAudio: boolean) {
135 return isAudio && extname === '.mp4' // We use .mp4 even for audio file only
136 ? MIMETYPES.AUDIO.EXT_MIMETYPE['.m4a']
137 : MIMETYPES.VIDEO.EXT_MIMETYPE[extname]
138}
139
133// --------------------------------------------------------------------------- 140// ---------------------------------------------------------------------------
134 141
135export { 142export {
@@ -140,5 +147,6 @@ export {
140 removeAllWebVideoFiles, 147 removeAllWebVideoFiles,
141 removeWebVideoFile, 148 removeWebVideoFile,
142 149
143 buildFileMetadata 150 buildFileMetadata,
151 getVideoFileMimeType
144} 152}
diff --git a/server/server/models/redundancy/video-redundancy.ts b/server/server/models/redundancy/video-redundancy.ts
index 26089594d..3a5e7bf33 100644
--- a/server/server/models/redundancy/video-redundancy.ts
+++ b/server/server/models/redundancy/video-redundancy.ts
@@ -15,6 +15,7 @@ import {
15 UpdatedAt 15 UpdatedAt
16} from 'sequelize-typescript' 16} from 'sequelize-typescript'
17import { 17import {
18 ActivityVideoUrlObject,
18 CacheFileObject, 19 CacheFileObject,
19 FileRedundancyInformation, 20 FileRedundancyInformation,
20 StreamingPlaylistRedundancyInformation, 21 StreamingPlaylistRedundancyInformation,
@@ -31,7 +32,7 @@ import { MActor, MVideoForRedundancyAPI, MVideoRedundancy, MVideoRedundancyAP, M
31import { isActivityPubUrlValid, isUrlValid } from '../../helpers/custom-validators/activitypub/misc.js' 32import { isActivityPubUrlValid, isUrlValid } from '../../helpers/custom-validators/activitypub/misc.js'
32import { logger } from '../../helpers/logger.js' 33import { logger } from '../../helpers/logger.js'
33import { CONFIG } from '../../initializers/config.js' 34import { CONFIG } from '../../initializers/config.js'
34import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../initializers/constants.js' 35import { CONSTRAINTS_FIELDS } from '../../initializers/constants.js'
35import { ActorModel } from '../actor/actor.js' 36import { ActorModel } from '../actor/actor.js'
36import { ServerModel } from '../server/server.js' 37import { ServerModel } from '../server/server.js'
37import { getSort, getVideoSort, parseAggregateResult, throwIfNotValid } from '../shared/index.js' 38import { getSort, getVideoSort, parseAggregateResult, throwIfNotValid } from '../shared/index.js'
@@ -40,6 +41,7 @@ import { VideoChannelModel } from '../video/video-channel.js'
40import { VideoFileModel } from '../video/video-file.js' 41import { VideoFileModel } from '../video/video-file.js'
41import { VideoStreamingPlaylistModel } from '../video/video-streaming-playlist.js' 42import { VideoStreamingPlaylistModel } from '../video/video-streaming-playlist.js'
42import { VideoModel } from '../video/video.js' 43import { VideoModel } from '../video/video.js'
44import { getVideoFileMimeType } from '@server/lib/video-file.js'
43 45
44export enum ScopeNames { 46export enum ScopeNames {
45 WITH_VIDEO = 'WITH_VIDEO' 47 WITH_VIDEO = 'WITH_VIDEO'
@@ -733,15 +735,19 @@ export class VideoRedundancyModel extends Model<Partial<AttributesOnly<VideoRedu
733 id: this.url, 735 id: this.url,
734 type: 'CacheFile' as 'CacheFile', 736 type: 'CacheFile' as 'CacheFile',
735 object: this.VideoFile.Video.url, 737 object: this.VideoFile.Video.url,
736 expires: this.expiresOn ? this.expiresOn.toISOString() : null, 738
739 expires: this.expiresOn
740 ? this.expiresOn.toISOString()
741 : null,
742
737 url: { 743 url: {
738 type: 'Link', 744 type: 'Link',
739 mediaType: MIMETYPES.VIDEO.EXT_MIMETYPE[this.VideoFile.extname] as any, 745 mediaType: getVideoFileMimeType(this.VideoFile.extname, this.VideoFile.isAudio()),
740 href: this.fileUrl, 746 href: this.fileUrl,
741 height: this.VideoFile.resolution, 747 height: this.VideoFile.resolution,
742 size: this.VideoFile.size, 748 size: this.VideoFile.size,
743 fps: this.VideoFile.fps 749 fps: this.VideoFile.fps
744 } 750 } as ActivityVideoUrlObject
745 } 751 }
746 } 752 }
747 753
diff --git a/server/server/models/video/formatter/video-activity-pub-format.ts b/server/server/models/video/formatter/video-activity-pub-format.ts
index d19bb1880..7beec741a 100644
--- a/server/server/models/video/formatter/video-activity-pub-format.ts
+++ b/server/server/models/video/formatter/video-activity-pub-format.ts
@@ -9,9 +9,10 @@ import {
9 ActivityTagObject, 9 ActivityTagObject,
10 ActivityTrackerUrlObject, 10 ActivityTrackerUrlObject,
11 ActivityUrlObject, 11 ActivityUrlObject,
12 ActivityVideoUrlObject,
12 VideoObject 13 VideoObject
13} from '@peertube/peertube-models' 14} from '@peertube/peertube-models'
14import { MIMETYPES, WEBSERVER } from '../../../initializers/constants.js' 15import { WEBSERVER } from '../../../initializers/constants.js'
15import { 16import {
16 getLocalVideoChaptersActivityPubUrl, 17 getLocalVideoChaptersActivityPubUrl,
17 getLocalVideoCommentsActivityPubUrl, 18 getLocalVideoCommentsActivityPubUrl,
@@ -23,6 +24,7 @@ import { MStreamingPlaylistFiles, MUserId, MVideo, MVideoAP, MVideoFile } from '
23import { VideoCaptionModel } from '../video-caption.js' 24import { VideoCaptionModel } from '../video-caption.js'
24import { sortByResolutionDesc } from './shared/index.js' 25import { sortByResolutionDesc } from './shared/index.js'
25import { getCategoryLabel, getLanguageLabel, getLicenceLabel } from './video-api-format.js' 26import { getCategoryLabel, getLanguageLabel, getLicenceLabel } from './video-api-format.js'
27import { getVideoFileMimeType } from '@server/lib/video-file.js'
26 28
27export function videoModelToActivityPubObject (video: MVideoAP): VideoObject { 29export function videoModelToActivityPubObject (video: MVideoAP): VideoObject {
28 const language = video.language 30 const language = video.language
@@ -179,18 +181,20 @@ function buildVideoFileUrls (options: {
179 .sort(sortByResolutionDesc) 181 .sort(sortByResolutionDesc)
180 182
181 for (const file of sortedFiles) { 183 for (const file of sortedFiles) {
184 const mimeType = getVideoFileMimeType(file.extname, file.isAudio())
185
182 urls.push({ 186 urls.push({
183 type: 'Link', 187 type: 'Link',
184 mediaType: MIMETYPES.VIDEO.EXT_MIMETYPE[file.extname] as any, 188 mediaType: mimeType,
185 href: file.getFileUrl(video), 189 href: file.getFileUrl(video),
186 height: file.resolution, 190 height: file.resolution,
187 size: file.size, 191 size: file.size,
188 fps: file.fps 192 fps: file.fps
189 }) 193 } as ActivityVideoUrlObject)
190 194
191 urls.push({ 195 urls.push({
192 type: 'Link', 196 type: 'Link',
193 rel: [ 'metadata', MIMETYPES.VIDEO.EXT_MIMETYPE[file.extname] ], 197 rel: [ 'metadata', mimeType ],
194 mediaType: 'application/json' as 'application/json', 198 mediaType: 'application/json' as 'application/json',
195 href: getLocalVideoFileMetadataUrl(video, file), 199 href: getLocalVideoFileMetadataUrl(video, file),
196 height: file.resolution, 200 height: file.resolution,