diff options
-rw-r--r-- | packages/tests/src/feeds/feeds.ts | 32 | ||||
-rw-r--r-- | server/server/controllers/feeds/video-feeds.ts | 7 | ||||
-rw-r--r-- | server/server/controllers/feeds/video-podcast-feeds.ts | 8 | ||||
-rw-r--r-- | server/server/initializers/constants.ts | 3 | ||||
-rw-r--r-- | server/server/lib/video-file.ts | 10 | ||||
-rw-r--r-- | server/server/models/redundancy/video-redundancy.ts | 14 | ||||
-rw-r--r-- | server/server/models/video/formatter/video-activity-pub-format.ts | 12 |
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' | |||
3 | import { Feed } from '@peertube/feed' | 3 | import { Feed } from '@peertube/feed' |
4 | import { cacheRouteFactory } from '@server/middlewares/index.js' | 4 | import { cacheRouteFactory } from '@server/middlewares/index.js' |
5 | import { VideoModel } from '@server/models/video/video.js' | 5 | import { VideoModel } from '@server/models/video/video.js' |
6 | import { VideoInclude } from '@peertube/peertube-models' | 6 | import { VideoInclude, VideoResolution } from '@peertube/peertube-models' |
7 | import { buildNSFWFilter } from '../../helpers/express-utils.js' | 7 | import { buildNSFWFilter } from '../../helpers/express-utils.js' |
8 | import { MIMETYPES, ROUTE_CACHE_LIFETIME, WEBSERVER } from '../../initializers/constants.js' | 8 | import { ROUTE_CACHE_LIFETIME, WEBSERVER } from '../../initializers/constants.js' |
9 | import { | 9 | import { |
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' |
19 | import { buildFeedMetadata, getCommonVideoFeedAttributes, getVideosForFeeds, initFeed, sendFeed } from './shared/index.js' | 19 | import { buildFeedMetadata, getCommonVideoFeedAttributes, getVideosForFeeds, initFeed, sendFeed } from './shared/index.js' |
20 | import { getVideoFileMimeType } from '@server/lib/video-file.js' | ||
20 | 21 | ||
21 | const videoFeedsRouter = express.Router() | 22 | const 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 | |||
15 | import { VideoModel } from '../../models/video/video.js' | 15 | import { VideoModel } from '../../models/video/video.js' |
16 | import { VideoCaptionModel } from '../../models/video/video-caption.js' | 16 | import { VideoCaptionModel } from '../../models/video/video-caption.js' |
17 | import { buildFeedMetadata, getCommonVideoFeedAttributes, getVideosForFeeds, initFeed } from './shared/index.js' | 17 | import { buildFeedMetadata, getCommonVideoFeedAttributes, getVideosForFeeds, initFeed } from './shared/index.js' |
18 | import { getVideoFileMimeType } from '@server/lib/video-file.js' | ||
18 | 19 | ||
19 | const videoPodcastFeedsRouter = express.Router() | 20 | const videoPodcastFeedsRouter = express.Router() |
20 | 21 | ||
@@ -243,11 +244,6 @@ async function addLivePodcastItem (options: { | |||
243 | // --------------------------------------------------------------------------- | 244 | // --------------------------------------------------------------------------- |
244 | 245 | ||
245 | function buildVODWebVideoFile (video: MVideo, videoFile: VideoFile) { | 246 | function 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 | |||
8 | import { lTags } from './object-storage/shared/index.js' | 8 | import { lTags } from './object-storage/shared/index.js' |
9 | import { generateHLSVideoFilename, generateWebVideoFilename } from './paths.js' | 9 | import { generateHLSVideoFilename, generateWebVideoFilename } from './paths.js' |
10 | import { VideoPathManager } from './video-path-manager.js' | 10 | import { VideoPathManager } from './video-path-manager.js' |
11 | import { MIMETYPES } from '@server/initializers/constants.js' | ||
11 | 12 | ||
12 | async function buildNewFile (options: { | 13 | async 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 | ||
134 | function 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 | ||
135 | export { | 142 | export { |
@@ -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' |
17 | import { | 17 | import { |
18 | ActivityVideoUrlObject, | ||
18 | CacheFileObject, | 19 | CacheFileObject, |
19 | FileRedundancyInformation, | 20 | FileRedundancyInformation, |
20 | StreamingPlaylistRedundancyInformation, | 21 | StreamingPlaylistRedundancyInformation, |
@@ -31,7 +32,7 @@ import { MActor, MVideoForRedundancyAPI, MVideoRedundancy, MVideoRedundancyAP, M | |||
31 | import { isActivityPubUrlValid, isUrlValid } from '../../helpers/custom-validators/activitypub/misc.js' | 32 | import { isActivityPubUrlValid, isUrlValid } from '../../helpers/custom-validators/activitypub/misc.js' |
32 | import { logger } from '../../helpers/logger.js' | 33 | import { logger } from '../../helpers/logger.js' |
33 | import { CONFIG } from '../../initializers/config.js' | 34 | import { CONFIG } from '../../initializers/config.js' |
34 | import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../initializers/constants.js' | 35 | import { CONSTRAINTS_FIELDS } from '../../initializers/constants.js' |
35 | import { ActorModel } from '../actor/actor.js' | 36 | import { ActorModel } from '../actor/actor.js' |
36 | import { ServerModel } from '../server/server.js' | 37 | import { ServerModel } from '../server/server.js' |
37 | import { getSort, getVideoSort, parseAggregateResult, throwIfNotValid } from '../shared/index.js' | 38 | import { getSort, getVideoSort, parseAggregateResult, throwIfNotValid } from '../shared/index.js' |
@@ -40,6 +41,7 @@ import { VideoChannelModel } from '../video/video-channel.js' | |||
40 | import { VideoFileModel } from '../video/video-file.js' | 41 | import { VideoFileModel } from '../video/video-file.js' |
41 | import { VideoStreamingPlaylistModel } from '../video/video-streaming-playlist.js' | 42 | import { VideoStreamingPlaylistModel } from '../video/video-streaming-playlist.js' |
42 | import { VideoModel } from '../video/video.js' | 43 | import { VideoModel } from '../video/video.js' |
44 | import { getVideoFileMimeType } from '@server/lib/video-file.js' | ||
43 | 45 | ||
44 | export enum ScopeNames { | 46 | export 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' |
14 | import { MIMETYPES, WEBSERVER } from '../../../initializers/constants.js' | 15 | import { WEBSERVER } from '../../../initializers/constants.js' |
15 | import { | 16 | import { |
16 | getLocalVideoChaptersActivityPubUrl, | 17 | getLocalVideoChaptersActivityPubUrl, |
17 | getLocalVideoCommentsActivityPubUrl, | 18 | getLocalVideoCommentsActivityPubUrl, |
@@ -23,6 +24,7 @@ import { MStreamingPlaylistFiles, MUserId, MVideo, MVideoAP, MVideoFile } from ' | |||
23 | import { VideoCaptionModel } from '../video-caption.js' | 24 | import { VideoCaptionModel } from '../video-caption.js' |
24 | import { sortByResolutionDesc } from './shared/index.js' | 25 | import { sortByResolutionDesc } from './shared/index.js' |
25 | import { getCategoryLabel, getLanguageLabel, getLicenceLabel } from './video-api-format.js' | 26 | import { getCategoryLabel, getLanguageLabel, getLicenceLabel } from './video-api-format.js' |
27 | import { getVideoFileMimeType } from '@server/lib/video-file.js' | ||
26 | 28 | ||
27 | export function videoModelToActivityPubObject (video: MVideoAP): VideoObject { | 29 | export 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, |