]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Add runner server tests
authorChocobozzz <me@florianbigard.com>
Fri, 21 Apr 2023 13:00:01 +0000 (15:00 +0200)
committerChocobozzz <chocobozzz@cpy.re>
Tue, 9 May 2023 06:57:34 +0000 (08:57 +0200)
95 files changed:
server/tests/api/activitypub/cleaner.ts
server/tests/api/activitypub/fetch.ts
server/tests/api/activitypub/refresher.ts
server/tests/api/activitypub/security.ts
server/tests/api/check-params/config.ts
server/tests/api/check-params/index.ts
server/tests/api/check-params/runners.ts [new file with mode: 0644]
server/tests/api/check-params/video-blacklist.ts
server/tests/api/check-params/video-playlists.ts
server/tests/api/check-params/videos.ts
server/tests/api/index.ts
server/tests/api/live/live.ts
server/tests/api/notifications/admin-notifications.ts
server/tests/api/object-storage/live.ts
server/tests/api/object-storage/video-static-file-privacy.ts
server/tests/api/object-storage/videos.ts
server/tests/api/runners/index.ts [new file with mode: 0644]
server/tests/api/runners/runner-common.ts [new file with mode: 0644]
server/tests/api/runners/runner-live-transcoding.ts [new file with mode: 0644]
server/tests/api/runners/runner-socket.ts [new file with mode: 0644]
server/tests/api/runners/runner-vod-transcoding.ts [new file with mode: 0644]
server/tests/api/server/config.ts
server/tests/api/server/follow-constraints.ts
server/tests/api/server/follows.ts
server/tests/api/server/handle-down.ts
server/tests/api/server/plugins.ts
server/tests/api/transcoding/audio-only.ts
server/tests/api/transcoding/transcoder.ts
server/tests/api/users/oauth.ts
server/tests/api/videos/multiple-servers.ts
server/tests/api/videos/resumable-upload.ts
server/tests/api/videos/single-server.ts
server/tests/api/videos/video-channel-syncs.ts
server/tests/api/videos/video-channels.ts
server/tests/api/videos/video-static-file-privacy.ts
server/tests/api/views/videos-views-cleaner.ts
server/tests/cli/create-transcoding-job.ts [deleted file]
server/tests/cli/index.ts
server/tests/cli/print-transcode-command.ts [deleted file]
server/tests/cli/update-host.ts
server/tests/fixtures/live/0-000067.ts [new file with mode: 0644]
server/tests/fixtures/live/0-000068.ts [new file with mode: 0644]
server/tests/fixtures/live/0-000069.ts [new file with mode: 0644]
server/tests/fixtures/live/0-000070.ts [new file with mode: 0644]
server/tests/fixtures/live/0.m3u8 [new file with mode: 0644]
server/tests/fixtures/live/1-000067.ts [new file with mode: 0644]
server/tests/fixtures/live/1-000068.ts [new file with mode: 0644]
server/tests/fixtures/live/1-000069.ts [new file with mode: 0644]
server/tests/fixtures/live/1-000070.ts [new file with mode: 0644]
server/tests/fixtures/live/1.m3u8 [new file with mode: 0644]
server/tests/fixtures/live/master.m3u8 [new file with mode: 0644]
server/tests/fixtures/video_short_0p.mp4 [new file with mode: 0644]
server/tests/fixtures/video_short_144p.m3u8 [new file with mode: 0644]
server/tests/fixtures/video_short_144p.mp4 [new file with mode: 0644]
server/tests/fixtures/video_short_240p.m3u8 [new file with mode: 0644]
server/tests/fixtures/video_short_240p.mp4
server/tests/fixtures/video_short_360p.m3u8 [new file with mode: 0644]
server/tests/fixtures/video_short_360p.mp4 [new file with mode: 0644]
server/tests/fixtures/video_short_480.webm [moved from server/tests/fixtures/video_short-480.webm with 100% similarity]
server/tests/fixtures/video_short_480p.m3u8 [new file with mode: 0644]
server/tests/fixtures/video_short_480p.mp4 [new file with mode: 0644]
server/tests/fixtures/video_short_720p.m3u8 [new file with mode: 0644]
server/tests/fixtures/video_short_720p.mp4 [new file with mode: 0644]
server/tests/index.ts
server/tests/lib/video-constant-registry-factory.ts
server/tests/peertube-runner/client-cli.ts [new file with mode: 0644]
server/tests/peertube-runner/index.ts [new file with mode: 0644]
server/tests/peertube-runner/live-transcoding.ts [new file with mode: 0644]
server/tests/peertube-runner/vod-transcoding.ts [new file with mode: 0644]
server/tests/plugins/plugin-transcoding.ts
server/tests/shared/checks.ts
server/tests/shared/generate.ts
server/tests/shared/index.ts
server/tests/shared/live.ts
server/tests/shared/peertube-runner-process.ts [new file with mode: 0644]
server/tests/shared/sql-command.ts [moved from shared/server-commands/miscs/sql-command.ts with 96% similarity]
server/tests/shared/streaming-playlists.ts
server/tests/shared/videos.ts
server/tests/shared/webtorrent.ts [moved from shared/server-commands/miscs/webtorrent.ts with 62% similarity]
server/tsconfig.json
shared/server-commands/index.ts
shared/server-commands/miscs/index.ts [deleted file]
shared/server-commands/requests/requests.ts
shared/server-commands/runners/index.ts [new file with mode: 0644]
shared/server-commands/runners/runner-jobs-command.ts [new file with mode: 0644]
shared/server-commands/runners/runner-registration-tokens-command.ts [new file with mode: 0644]
shared/server-commands/runners/runners-command.ts [new file with mode: 0644]
shared/server-commands/server/config-command.ts
shared/server-commands/server/jobs.ts
shared/server-commands/server/server.ts
shared/server-commands/server/servers.ts
shared/server-commands/shared/abstract-command.ts
shared/server-commands/socket/socket-io-command.ts
shared/server-commands/videos/live-command.ts
shared/server-commands/videos/streaming-playlists-command.ts

index 1c1495022c5a5d0ce0cd9628ecd8db55295f7241..d67175e20bcfe1fe3461bdb2f7da8615d986f6ab 100644 (file)
@@ -1,6 +1,7 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
 import { expect } from 'chai'
+import { SQLCommand } from '@server/tests/shared'
 import { wait } from '@shared/core-utils'
 import {
   cleanupTests,
@@ -13,6 +14,8 @@ import {
 
 describe('Test AP cleaner', function () {
   let servers: PeerTubeServer[] = []
+  const sqlCommands: SQLCommand[] = []
+
   let videoUUID1: string
   let videoUUID2: string
   let videoUUID3: string
@@ -56,6 +59,8 @@ describe('Test AP cleaner', function () {
         await server.videos.rate({ id: uuid, rating: 'like' })
         await server.comments.createThread({ videoId: uuid, text: 'comment' })
       }
+
+      sqlCommands.push(new SQLCommand(server))
     }
 
     await waitJobs(servers)
@@ -75,9 +80,9 @@ describe('Test AP cleaner', function () {
   it('Should destroy server 3 internal likes and correctly clean them', async function () {
     this.timeout(20000)
 
-    await servers[2].sql.deleteAll('accountVideoRate')
+    await sqlCommands[2].deleteAll('accountVideoRate')
     for (const uuid of videoUUIDs) {
-      await servers[2].sql.setVideoField(uuid, 'likes', '0')
+      await sqlCommands[2].setVideoField(uuid, 'likes', '0')
     }
 
     await wait(5000)
@@ -121,10 +126,10 @@ describe('Test AP cleaner', function () {
   it('Should destroy server 3 internal dislikes and correctly clean them', async function () {
     this.timeout(20000)
 
-    await servers[2].sql.deleteAll('accountVideoRate')
+    await sqlCommands[2].deleteAll('accountVideoRate')
 
     for (const uuid of videoUUIDs) {
-      await servers[2].sql.setVideoField(uuid, 'dislikes', '0')
+      await sqlCommands[2].setVideoField(uuid, 'dislikes', '0')
     }
 
     await wait(5000)
@@ -148,15 +153,15 @@ describe('Test AP cleaner', function () {
   it('Should destroy server 3 internal shares and correctly clean them', async function () {
     this.timeout(20000)
 
-    const preCount = await servers[0].sql.getVideoShareCount()
+    const preCount = await sqlCommands[0].getVideoShareCount()
     expect(preCount).to.equal(6)
 
-    await servers[2].sql.deleteAll('videoShare')
+    await sqlCommands[2].deleteAll('videoShare')
     await wait(5000)
     await waitJobs(servers)
 
     // Still 6 because we don't have remote shares on local videos
-    const postCount = await servers[0].sql.getVideoShareCount()
+    const postCount = await sqlCommands[0].getVideoShareCount()
     expect(postCount).to.equal(6)
   })
 
@@ -168,7 +173,7 @@ describe('Test AP cleaner', function () {
       expect(total).to.equal(3)
     }
 
-    await servers[2].sql.deleteAll('videoComment')
+    await sqlCommands[2].deleteAll('videoComment')
 
     await wait(5000)
     await waitJobs(servers)
@@ -185,7 +190,7 @@ describe('Test AP cleaner', function () {
     async function check (like: string, ofServerUrl: string, urlSuffix: string, remote: 'true' | 'false') {
       const query = `SELECT "videoId", "accountVideoRate".url FROM "accountVideoRate" ` +
         `INNER JOIN video ON "accountVideoRate"."videoId" = video.id AND remote IS ${remote} WHERE "accountVideoRate"."url" LIKE '${like}'`
-      const res = await servers[0].sql.selectQuery<{ url: string }>(query)
+      const res = await sqlCommands[0].selectQuery<{ url: string }>(query)
 
       for (const rate of res) {
         const matcher = new RegExp(`^${ofServerUrl}/accounts/root/dislikes/\\d+${urlSuffix}$`)
@@ -214,7 +219,7 @@ describe('Test AP cleaner', function () {
 
     {
       const query = `UPDATE "accountVideoRate" SET url = url || 'stan'`
-      await servers[1].sql.updateQuery(query)
+      await sqlCommands[1].updateQuery(query)
 
       await wait(5000)
       await waitJobs(servers)
@@ -231,7 +236,7 @@ describe('Test AP cleaner', function () {
       const query = `SELECT "videoId", "videoComment".url, uuid as "videoUUID" FROM "videoComment" ` +
         `INNER JOIN video ON "videoComment"."videoId" = video.id AND remote IS ${remote} WHERE "videoComment"."url" LIKE '${like}'`
 
-      const res = await servers[0].sql.selectQuery<{ url: string, videoUUID: string }>(query)
+      const res = await sqlCommands[0].selectQuery<{ url: string, videoUUID: string }>(query)
 
       for (const comment of res) {
         const matcher = new RegExp(`${ofServerUrl}/videos/watch/${comment.videoUUID}/comments/\\d+${urlSuffix}`)
@@ -257,7 +262,7 @@ describe('Test AP cleaner', function () {
 
     {
       const query = `UPDATE "videoComment" SET url = url || 'kyle'`
-      await servers[1].sql.updateQuery(query)
+      await sqlCommands[1].updateQuery(query)
 
       await wait(5000)
       await waitJobs(servers)
@@ -328,6 +333,10 @@ describe('Test AP cleaner', function () {
   })
 
   after(async function () {
+    for (const sql of sqlCommands) {
+      await sql.cleanup()
+    }
+
     await cleanupTests(servers)
   })
 })
index f0caea507d1ac74fba8dadca7e20c31e60747c2e..3899a6a49c37d9bacb0d9df42a124a86ba1d9fc2 100644 (file)
@@ -1,6 +1,7 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
 import { expect } from 'chai'
+import { SQLCommand } from '@server/tests/shared'
 import {
   cleanupTests,
   createMultipleServers,
@@ -12,6 +13,7 @@ import {
 
 describe('Test ActivityPub fetcher', function () {
   let servers: PeerTubeServer[]
+  let sqlCommandServer1: SQLCommand
 
   // ---------------------------------------------------------------
 
@@ -34,15 +36,17 @@ describe('Test ActivityPub fetcher', function () {
     const { uuid } = await servers[0].videos.upload({ attributes: { name: 'bad video root' } })
     await servers[0].videos.upload({ token: userAccessToken, attributes: { name: 'video user' } })
 
+    sqlCommandServer1 = new SQLCommand(servers[0])
+
     {
       const to = servers[0].url + '/accounts/user1'
       const value = servers[1].url + '/accounts/user1'
-      await servers[0].sql.setActorField(to, 'url', value)
+      await sqlCommandServer1.setActorField(to, 'url', value)
     }
 
     {
       const value = servers[2].url + '/videos/watch/' + uuid
-      await servers[0].sql.setVideoField(uuid, 'url', value)
+      await sqlCommandServer1.setVideoField(uuid, 'url', value)
     }
   })
 
@@ -72,6 +76,7 @@ describe('Test ActivityPub fetcher', function () {
   after(async function () {
     this.timeout(20000)
 
+    await sqlCommandServer1.cleanup()
     await cleanupTests(servers)
   })
 })
index 4fb22f512a010790ac8e0534a239c3123da2ec62..6c48b7ac8d312613520a27478e51a25cfd1ab777 100644 (file)
@@ -1,5 +1,6 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
+import { SQLCommand } from '@server/tests/shared'
 import { wait } from '@shared/core-utils'
 import { HttpStatusCode, VideoPlaylistPrivacy } from '@shared/models'
 import {
@@ -15,6 +16,7 @@ import {
 
 describe('Test AP refresher', function () {
   let servers: PeerTubeServer[] = []
+  let sqlCommandServer2: SQLCommand
   let videoUUID1: string
   let videoUUID2: string
   let videoUUID3: string
@@ -61,6 +63,8 @@ describe('Test AP refresher', function () {
     }
 
     await doubleFollow(servers[0], servers[1])
+
+    sqlCommandServer2 = new SQLCommand(servers[1])
   })
 
   describe('Videos refresher', function () {
@@ -71,7 +75,7 @@ describe('Test AP refresher', function () {
       await wait(10000)
 
       // Change UUID so the remote server returns a 404
-      await servers[1].sql.setVideoField(videoUUID1, 'uuid', '304afe4f-39f9-4d49-8ed7-ac57b86b174f')
+      await sqlCommandServer2.setVideoField(videoUUID1, 'uuid', '304afe4f-39f9-4d49-8ed7-ac57b86b174f')
 
       await servers[0].videos.get({ id: videoUUID1 })
       await servers[0].videos.get({ id: videoUUID2 })
@@ -87,7 +91,7 @@ describe('Test AP refresher', function () {
 
       await killallServers([ servers[1] ])
 
-      await servers[1].sql.setVideoField(videoUUID3, 'uuid', '304afe4f-39f9-4d49-8ed7-ac57b86b174e')
+      await sqlCommandServer2.setVideoField(videoUUID3, 'uuid', '304afe4f-39f9-4d49-8ed7-ac57b86b174e')
 
       // Video will need a refresh
       await wait(10000)
@@ -113,7 +117,7 @@ describe('Test AP refresher', function () {
 
       // Change actor name so the remote server returns a 404
       const to = servers[1].url + '/accounts/user2'
-      await servers[1].sql.setActorField(to, 'preferredUsername', 'toto')
+      await sqlCommandServer2.setActorField(to, 'preferredUsername', 'toto')
 
       await command.get({ accountName: 'user1@' + servers[1].host })
       await command.get({ accountName: 'user2@' + servers[1].host })
@@ -133,7 +137,7 @@ describe('Test AP refresher', function () {
       await wait(10000)
 
       // Change UUID so the remote server returns a 404
-      await servers[1].sql.setPlaylistField(playlistUUID2, 'uuid', '304afe4f-39f9-4d49-8ed7-ac57b86b178e')
+      await sqlCommandServer2.setPlaylistField(playlistUUID2, 'uuid', '304afe4f-39f9-4d49-8ed7-ac57b86b178e')
 
       await servers[0].playlists.get({ playlistId: playlistUUID1 })
       await servers[0].playlists.get({ playlistId: playlistUUID2 })
@@ -148,6 +152,8 @@ describe('Test AP refresher', function () {
   after(async function () {
     this.timeout(10000)
 
+    await sqlCommandServer2.cleanup()
+
     await cleanupTests(servers)
   })
 })
index c6f1716336030f1de3f569a90483ab6cf8055205..d6a07b87f910d34c78b8e87c8163ede3c1911074 100644 (file)
@@ -5,26 +5,26 @@ import { buildDigest } from '@server/helpers/peertube-crypto'
 import { ACTIVITY_PUB, HTTP_SIGNATURE } from '@server/initializers/constants'
 import { activityPubContextify } from '@server/lib/activitypub/context'
 import { buildGlobalHeaders, signAndContextify } from '@server/lib/activitypub/send'
-import { makePOSTAPRequest } from '@server/tests/shared'
+import { makePOSTAPRequest, SQLCommand } from '@server/tests/shared'
 import { buildAbsoluteFixturePath, wait } from '@shared/core-utils'
 import { HttpStatusCode } from '@shared/models'
 import { cleanupTests, createMultipleServers, killallServers, PeerTubeServer } from '@shared/server-commands'
 
-function setKeysOfServer (onServer: PeerTubeServer, ofServer: PeerTubeServer, publicKey: string, privateKey: string) {
-  const url = ofServer.url + '/accounts/peertube'
+function setKeysOfServer (onServer: SQLCommand, ofServerUrl: string, publicKey: string, privateKey: string) {
+  const url = ofServerUrl + '/accounts/peertube'
 
   return Promise.all([
-    onServer.sql.setActorField(url, 'publicKey', publicKey),
-    onServer.sql.setActorField(url, 'privateKey', privateKey)
+    onServer.setActorField(url, 'publicKey', publicKey),
+    onServer.setActorField(url, 'privateKey', privateKey)
   ])
 }
 
-function setUpdatedAtOfServer (onServer: PeerTubeServer, ofServer: PeerTubeServer, updatedAt: string) {
-  const url = ofServer.url + '/accounts/peertube'
+function setUpdatedAtOfServer (onServer: SQLCommand, ofServerUrl: string, updatedAt: string) {
+  const url = ofServerUrl + '/accounts/peertube'
 
   return Promise.all([
-    onServer.sql.setActorField(url, 'createdAt', updatedAt),
-    onServer.sql.setActorField(url, 'updatedAt', updatedAt)
+    onServer.setActorField(url, 'createdAt', updatedAt),
+    onServer.setActorField(url, 'updatedAt', updatedAt)
   ])
 }
 
@@ -71,6 +71,8 @@ async function makeFollowRequest (to: { url: string }, by: { url: string, privat
 
 describe('Test ActivityPub security', function () {
   let servers: PeerTubeServer[]
+  let sqlCommands: SQLCommand[]
+
   let url: string
 
   const keys = require(buildAbsoluteFixturePath('./ap-json/peertube/keys.json'))
@@ -90,10 +92,12 @@ describe('Test ActivityPub security', function () {
 
     servers = await createMultipleServers(3)
 
+    sqlCommands = servers.map(s => new SQLCommand(s))
+
     url = servers[0].url + '/inbox'
 
-    await setKeysOfServer(servers[0], servers[1], keys.publicKey, null)
-    await setKeysOfServer(servers[1], servers[1], keys.publicKey, keys.privateKey)
+    await setKeysOfServer(sqlCommands[0], servers[1].url, keys.publicKey, null)
+    await setKeysOfServer(sqlCommands[1], servers[1].url, keys.publicKey, keys.privateKey)
 
     const to = { url: servers[0].url + '/accounts/peertube' }
     const by = { url: servers[1].url + '/accounts/peertube', privateKey: keys.privateKey }
@@ -130,8 +134,8 @@ describe('Test ActivityPub security', function () {
     })
 
     it('Should fail with bad keys', async function () {
-      await setKeysOfServer(servers[0], servers[1], invalidKeys.publicKey, invalidKeys.privateKey)
-      await setKeysOfServer(servers[1], servers[1], invalidKeys.publicKey, invalidKeys.privateKey)
+      await setKeysOfServer(sqlCommands[0], servers[1].url, invalidKeys.publicKey, invalidKeys.privateKey)
+      await setKeysOfServer(sqlCommands[1], servers[1].url, invalidKeys.publicKey, invalidKeys.privateKey)
 
       const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce')
       const headers = buildGlobalHeaders(body)
@@ -145,8 +149,8 @@ describe('Test ActivityPub security', function () {
     })
 
     it('Should reject requests without appropriate signed headers', async function () {
-      await setKeysOfServer(servers[0], servers[1], keys.publicKey, keys.privateKey)
-      await setKeysOfServer(servers[1], servers[1], keys.publicKey, keys.privateKey)
+      await setKeysOfServer(sqlCommands[0], servers[1].url, keys.publicKey, keys.privateKey)
+      await setKeysOfServer(sqlCommands[1], servers[1].url, keys.publicKey, keys.privateKey)
 
       const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce')
       const headers = buildGlobalHeaders(body)
@@ -194,8 +198,8 @@ describe('Test ActivityPub security', function () {
 
       // Update keys of server 2 to invalid keys
       // Server 1 should refresh the actor and fail
-      await setKeysOfServer(servers[1], servers[1], invalidKeys.publicKey, invalidKeys.privateKey)
-      await setUpdatedAtOfServer(servers[0], servers[1], '2015-07-17 22:00:00+00')
+      await setKeysOfServer(sqlCommands[1], servers[1].url, invalidKeys.publicKey, invalidKeys.privateKey)
+      await setUpdatedAtOfServer(sqlCommands[0], servers[1].url, '2015-07-17 22:00:00+00')
 
       // Invalid peertube actor cache
       await killallServers([ servers[1] ])
@@ -218,9 +222,9 @@ describe('Test ActivityPub security', function () {
     before(async function () {
       this.timeout(10000)
 
-      await setKeysOfServer(servers[0], servers[1], keys.publicKey, keys.privateKey)
-      await setKeysOfServer(servers[1], servers[1], keys.publicKey, keys.privateKey)
-      await setKeysOfServer(servers[2], servers[2], keys.publicKey, keys.privateKey)
+      await setKeysOfServer(sqlCommands[0], servers[1].url, keys.publicKey, keys.privateKey)
+      await setKeysOfServer(sqlCommands[1], servers[1].url, keys.publicKey, keys.privateKey)
+      await setKeysOfServer(sqlCommands[2], servers[2].url, keys.publicKey, keys.privateKey)
 
       const to = { url: servers[0].url + '/accounts/peertube' }
       const by = { url: servers[2].url + '/accounts/peertube', privateKey: keys.privateKey }
@@ -230,8 +234,8 @@ describe('Test ActivityPub security', function () {
     it('Should fail with bad keys', async function () {
       this.timeout(10000)
 
-      await setKeysOfServer(servers[0], servers[2], invalidKeys.publicKey, invalidKeys.privateKey)
-      await setKeysOfServer(servers[2], servers[2], invalidKeys.publicKey, invalidKeys.privateKey)
+      await setKeysOfServer(sqlCommands[0], servers[2].url, invalidKeys.publicKey, invalidKeys.privateKey)
+      await setKeysOfServer(sqlCommands[2], servers[2].url, invalidKeys.publicKey, invalidKeys.privateKey)
 
       const body = getAnnounceWithoutContext(servers[1])
       body.actor = servers[2].url + '/accounts/peertube'
@@ -252,8 +256,8 @@ describe('Test ActivityPub security', function () {
     it('Should fail with an altered body', async function () {
       this.timeout(10000)
 
-      await setKeysOfServer(servers[0], servers[2], keys.publicKey, keys.privateKey)
-      await setKeysOfServer(servers[0], servers[2], keys.publicKey, keys.privateKey)
+      await setKeysOfServer(sqlCommands[0], servers[2].url, keys.publicKey, keys.privateKey)
+      await setKeysOfServer(sqlCommands[0], servers[2].url, keys.publicKey, keys.privateKey)
 
       const body = getAnnounceWithoutContext(servers[1])
       body.actor = servers[2].url + '/accounts/peertube'
@@ -296,7 +300,7 @@ describe('Test ActivityPub security', function () {
 
       // Update keys of server 3 to invalid keys
       // Server 1 should refresh the actor and fail
-      await setKeysOfServer(servers[2], servers[2], invalidKeys.publicKey, invalidKeys.privateKey)
+      await setKeysOfServer(sqlCommands[2], servers[2].url, invalidKeys.publicKey, invalidKeys.privateKey)
 
       const body = getAnnounceWithoutContext(servers[1])
       body.actor = servers[2].url + '/accounts/peertube'
@@ -316,7 +320,9 @@ describe('Test ActivityPub security', function () {
   })
 
   after(async function () {
-    this.timeout(10000)
+    for (const sql of sqlCommands) {
+      await sql.cleanup()
+    }
 
     await cleanupTests(servers)
   })
index f49a4b86814e4b10c0b70cc24af86b81bbf80c12..c5cda203e8d2a3e2872e12e041aeeebfb22fa354 100644 (file)
@@ -103,6 +103,9 @@ describe('Test config API validators', function () {
     },
     transcoding: {
       enabled: true,
+      remoteRunners: {
+        enabled: true
+      },
       allowAdditionalExtensions: true,
       allowAudioFiles: true,
       concurrency: 1,
@@ -140,6 +143,9 @@ describe('Test config API validators', function () {
 
       transcoding: {
         enabled: true,
+        remoteRunners: {
+          enabled: true
+        },
         threads: 4,
         profile: 'live_profile',
         resolutions: {
index ddbcb42f8d3db622bdd6cb508daed3940db4323e..400d312d3b9c6f5282fb16f2920ad37d577feb78 100644 (file)
@@ -16,6 +16,7 @@ import './my-user'
 import './plugins'
 import './redundancy'
 import './registrations'
+import './runners'
 import './search'
 import './services'
 import './transcoding'
diff --git a/server/tests/api/check-params/runners.ts b/server/tests/api/check-params/runners.ts
new file mode 100644 (file)
index 0000000..4da6fd9
--- /dev/null
@@ -0,0 +1,702 @@
+/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
+import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@server/tests/shared'
+import { HttpStatusCode, RunnerJob, RunnerJobState, RunnerJobSuccessPayload, RunnerJobUpdatePayload, VideoPrivacy } from '@shared/models'
+import {
+  cleanupTests,
+  createSingleServer,
+  makePostBodyRequest,
+  PeerTubeServer,
+  sendRTMPStream,
+  setAccessTokensToServers,
+  setDefaultVideoChannel,
+  stopFfmpeg,
+  waitJobs
+} from '@shared/server-commands'
+
+const badUUID = '910ec12a-d9e6-458b-a274-0abb655f9464'
+
+describe('Test managing runners', function () {
+  let server: PeerTubeServer
+
+  let userToken: string
+
+  let registrationTokenId: number
+  let registrationToken: string
+
+  let runnerToken: string
+  let runnerToken2: string
+
+  let completedJobToken: string
+  let completedJobUUID: string
+
+  let cancelledJobUUID: string
+
+  before(async function () {
+    this.timeout(120000)
+
+    const config = {
+      rates_limit: {
+        api: {
+          max: 5000
+        }
+      }
+    }
+
+    server = await createSingleServer(1, config)
+    await setAccessTokensToServers([ server ])
+    await setDefaultVideoChannel([ server ])
+
+    userToken = await server.users.generateUserAndToken('user1')
+
+    const { data } = await server.runnerRegistrationTokens.list()
+    registrationToken = data[0].registrationToken
+    registrationTokenId = data[0].id
+
+    await server.config.enableTranscoding(true, true)
+    await server.config.enableRemoteTranscoding()
+    runnerToken = await server.runners.autoRegisterRunner()
+    runnerToken2 = await server.runners.autoRegisterRunner()
+
+    {
+      await server.videos.quickUpload({ name: 'video 1' })
+      await server.videos.quickUpload({ name: 'video 2' })
+
+      await waitJobs([ server ])
+
+      {
+        const job = await server.runnerJobs.autoProcessWebVideoJob(runnerToken)
+        completedJobToken = job.jobToken
+        completedJobUUID = job.uuid
+      }
+
+      {
+        const { job } = await server.runnerJobs.autoAccept({ runnerToken })
+        cancelledJobUUID = job.uuid
+        await server.runnerJobs.cancelByAdmin({ jobUUID: cancelledJobUUID })
+      }
+    }
+  })
+
+  describe('Managing runner registration tokens', function () {
+
+    describe('Common', function () {
+
+      it('Should fail to generate, list or delete runner registration token without oauth token', async function () {
+        const expectedStatus = HttpStatusCode.UNAUTHORIZED_401
+
+        await server.runnerRegistrationTokens.generate({ token: null, expectedStatus })
+        await server.runnerRegistrationTokens.list({ token: null, expectedStatus })
+        await server.runnerRegistrationTokens.delete({ token: null, id: registrationTokenId, expectedStatus })
+      })
+
+      it('Should fail to generate, list or delete runner registration token without admin rights', async function () {
+        const expectedStatus = HttpStatusCode.FORBIDDEN_403
+
+        await server.runnerRegistrationTokens.generate({ token: userToken, expectedStatus })
+        await server.runnerRegistrationTokens.list({ token: userToken, expectedStatus })
+        await server.runnerRegistrationTokens.delete({ token: userToken, id: registrationTokenId, expectedStatus })
+      })
+    })
+
+    describe('Delete', function () {
+
+      it('Should fail to delete with a bad id', async function () {
+        await server.runnerRegistrationTokens.delete({ id: 404, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
+      })
+    })
+
+    describe('List', function () {
+      const path = '/api/v1/runners/registration-tokens'
+
+      it('Should fail to list with a bad start pagination', async function () {
+        await checkBadStartPagination(server.url, path, server.accessToken)
+      })
+
+      it('Should fail to list with a bad count pagination', async function () {
+        await checkBadCountPagination(server.url, path, server.accessToken)
+      })
+
+      it('Should fail to list with an incorrect sort', async function () {
+        await checkBadSortPagination(server.url, path, server.accessToken)
+      })
+
+      it('Should succeed to list with the correct params', async function () {
+        await server.runnerRegistrationTokens.list({ start: 0, count: 5, sort: '-createdAt' })
+      })
+    })
+  })
+
+  describe('Managing runners', function () {
+    let toDeleteId: number
+
+    describe('Register', function () {
+      const name = 'runner name'
+
+      it('Should fail with a bad registration token', async function () {
+        const expectedStatus = HttpStatusCode.BAD_REQUEST_400
+
+        await server.runners.register({ name, registrationToken: 'a'.repeat(4000), expectedStatus })
+        await server.runners.register({ name, registrationToken: null, expectedStatus })
+      })
+
+      it('Should fail with an unknown registration token', async function () {
+        await server.runners.register({ name, registrationToken: 'aaa', expectedStatus: HttpStatusCode.NOT_FOUND_404 })
+      })
+
+      it('Should fail with a bad name', async function () {
+        const expectedStatus = HttpStatusCode.BAD_REQUEST_400
+
+        await server.runners.register({ name: '', registrationToken, expectedStatus })
+        await server.runners.register({ name: 'a'.repeat(200), registrationToken, expectedStatus })
+      })
+
+      it('Should fail with an invalid description', async function () {
+        const expectedStatus = HttpStatusCode.BAD_REQUEST_400
+
+        await server.runners.register({ name, description: '', registrationToken, expectedStatus })
+        await server.runners.register({ name, description: 'a'.repeat(5000), registrationToken, expectedStatus })
+      })
+
+      it('Should succeed with the correct params', async function () {
+        const { id } = await server.runners.register({ name, description: 'super description', registrationToken })
+
+        toDeleteId = id
+      })
+    })
+
+    describe('Delete', function () {
+
+      it('Should fail without oauth token', async function () {
+        await server.runners.delete({ token: null, id: toDeleteId, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
+      })
+
+      it('Should fail without admin rights', async function () {
+        await server.runners.delete({ token: userToken, id: toDeleteId, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
+      })
+
+      it('Should fail with a bad id', async function () {
+        await server.runners.delete({ id: 'hi' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
+      })
+
+      it('Should fail with an unknown id', async function () {
+        await server.runners.delete({ id: 404, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
+      })
+
+      it('Should succeed with the correct params', async function () {
+        await server.runners.delete({ id: toDeleteId })
+      })
+    })
+
+    describe('List', function () {
+      const path = '/api/v1/runners'
+
+      it('Should fail without oauth token', async function () {
+        await server.runners.list({ token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
+      })
+
+      it('Should fail without admin rights', async function () {
+        await server.runners.list({ token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
+      })
+
+      it('Should fail to list with a bad start pagination', async function () {
+        await checkBadStartPagination(server.url, path, server.accessToken)
+      })
+
+      it('Should fail to list with a bad count pagination', async function () {
+        await checkBadCountPagination(server.url, path, server.accessToken)
+      })
+
+      it('Should fail to list with an incorrect sort', async function () {
+        await checkBadSortPagination(server.url, path, server.accessToken)
+      })
+
+      it('Should succeed to list with the correct params', async function () {
+        await server.runners.list({ start: 0, count: 5, sort: '-createdAt' })
+      })
+    })
+
+  })
+
+  describe('Runner jobs by admin', function () {
+
+    describe('Cancel', function () {
+      let jobUUID: string
+
+      before(async function () {
+        this.timeout(60000)
+
+        await server.videos.quickUpload({ name: 'video' })
+        await waitJobs([ server ])
+
+        const { availableJobs } = await server.runnerJobs.request({ runnerToken })
+        jobUUID = availableJobs[0].uuid
+      })
+
+      it('Should fail without oauth token', async function () {
+        await server.runnerJobs.cancelByAdmin({ token: null, jobUUID, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
+      })
+
+      it('Should fail without admin rights', async function () {
+        await server.runnerJobs.cancelByAdmin({ token: userToken, jobUUID, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
+      })
+
+      it('Should fail with a bad job uuid', async function () {
+        await server.runnerJobs.cancelByAdmin({ jobUUID: 'hello', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
+      })
+
+      it('Should fail with an unknown job uuid', async function () {
+        const jobUUID = badUUID
+        await server.runnerJobs.cancelByAdmin({ jobUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
+      })
+
+      it('Should succeed with the correct params', async function () {
+        await server.runnerJobs.cancelByAdmin({ jobUUID })
+      })
+    })
+
+    describe('List', function () {
+      const path = '/api/v1/runners/jobs'
+
+      it('Should fail without oauth token', async function () {
+        await server.runnerJobs.list({ token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
+      })
+
+      it('Should fail without admin rights', async function () {
+        await server.runnerJobs.list({ token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
+      })
+
+      it('Should fail to list with a bad start pagination', async function () {
+        await checkBadStartPagination(server.url, path, server.accessToken)
+      })
+
+      it('Should fail to list with a bad count pagination', async function () {
+        await checkBadCountPagination(server.url, path, server.accessToken)
+      })
+
+      it('Should fail to list with an incorrect sort', async function () {
+        await checkBadSortPagination(server.url, path, server.accessToken)
+      })
+
+      it('Should succeed to list with the correct params', async function () {
+        await server.runnerJobs.list({ start: 0, count: 5, sort: '-createdAt' })
+      })
+    })
+
+  })
+
+  describe('Runner jobs by runners', function () {
+    let jobUUID: string
+    let jobToken: string
+    let videoUUID: string
+
+    let jobUUID2: string
+    let jobToken2: string
+
+    let videoUUID2: string
+
+    let pendingUUID: string
+
+    let liveAcceptedJob: RunnerJob & { jobToken: string }
+
+    async function fetchFiles (options: {
+      jobUUID: string
+      videoUUID: string
+      runnerToken: string
+      jobToken: string
+      expectedStatus: HttpStatusCode
+    }) {
+      const { jobUUID, expectedStatus, videoUUID, runnerToken, jobToken } = options
+
+      const basePath = '/api/v1/runners/jobs/' + jobUUID + '/files/videos/' + videoUUID
+      const paths = [ `${basePath}/max-quality`, `${basePath}/previews/max-quality` ]
+
+      for (const path of paths) {
+        await makePostBodyRequest({ url: server.url, path, fields: { runnerToken, jobToken }, expectedStatus })
+      }
+    }
+
+    before(async function () {
+      this.timeout(120000)
+
+      {
+        await server.runnerJobs.cancelAllJobs({ state: RunnerJobState.PENDING })
+      }
+
+      {
+        const { uuid } = await server.videos.quickUpload({ name: 'video' })
+        videoUUID = uuid
+
+        await waitJobs([ server ])
+
+        const { job } = await server.runnerJobs.autoAccept({ runnerToken })
+        jobUUID = job.uuid
+        jobToken = job.jobToken
+      }
+
+      {
+        const { uuid } = await server.videos.quickUpload({ name: 'video' })
+        videoUUID2 = uuid
+
+        await waitJobs([ server ])
+
+        const { job } = await server.runnerJobs.autoAccept({ runnerToken: runnerToken2 })
+        jobUUID2 = job.uuid
+        jobToken2 = job.jobToken
+      }
+
+      {
+        await server.videos.quickUpload({ name: 'video' })
+        await waitJobs([ server ])
+
+        const { availableJobs } = await server.runnerJobs.request({ runnerToken })
+        pendingUUID = availableJobs[0].uuid
+      }
+
+      {
+        await server.config.enableLive({
+          allowReplay: false,
+          resolutions: 'max',
+          transcoding: true
+        })
+
+        const { live } = await server.live.quickCreate({ permanentLive: true, saveReplay: false, privacy: VideoPrivacy.PUBLIC })
+
+        const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey })
+        await waitJobs([ server ])
+
+        await server.runnerJobs.requestLiveJob(runnerToken)
+
+        const { job } = await server.runnerJobs.autoAccept({ runnerToken, type: 'live-rtmp-hls-transcoding' })
+        liveAcceptedJob = job
+
+        await stopFfmpeg(ffmpegCommand)
+      }
+    })
+
+    describe('Common runner tokens validations', function () {
+
+      async function testEndpoints (options: {
+        jobUUID: string
+        runnerToken: string
+        jobToken: string
+        expectedStatus: HttpStatusCode
+      }) {
+        await fetchFiles({ ...options, videoUUID })
+
+        await server.runnerJobs.abort({ ...options, reason: 'reason' })
+        await server.runnerJobs.update({ ...options })
+        await server.runnerJobs.error({ ...options, message: 'message' })
+        await server.runnerJobs.success({ ...options, payload: { videoFile: 'video_short.mp4' } })
+      }
+
+      it('Should fail with an invalid job uuid', async function () {
+        await testEndpoints({ jobUUID: 'a', runnerToken, jobToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
+      })
+
+      it('Should fail with an unknown job uuid', async function () {
+        const jobUUID = badUUID
+        await testEndpoints({ jobUUID, runnerToken, jobToken, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
+      })
+
+      it('Should fail with an invalid runner token', async function () {
+        await testEndpoints({ jobUUID, runnerToken: '', jobToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
+      })
+
+      it('Should fail with an unknown runner token', async function () {
+        const runnerToken = badUUID
+        await testEndpoints({ jobUUID, runnerToken, jobToken, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
+      })
+
+      it('Should fail with an invalid job token job uuid', async function () {
+        await testEndpoints({ jobUUID, runnerToken, jobToken: '', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
+      })
+
+      it('Should fail with an unknown job token job uuid', async function () {
+        const jobToken = badUUID
+        await testEndpoints({ jobUUID, runnerToken, jobToken, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
+      })
+
+      it('Should fail with a runner token not associated to this job', async function () {
+        await testEndpoints({ jobUUID, runnerToken: runnerToken2, jobToken, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
+      })
+
+      it('Should fail with a job uuid not associated to the job token', async function () {
+        await testEndpoints({ jobUUID: jobUUID2, runnerToken, jobToken, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
+        await testEndpoints({ jobUUID, runnerToken, jobToken: jobToken2, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
+      })
+    })
+
+    describe('Unregister', function () {
+
+      it('Should fail without a runner token', async function () {
+        await server.runners.unregister({ runnerToken: null, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
+      })
+
+      it('Should fail with a bad a runner token', async function () {
+        await server.runners.unregister({ runnerToken: '', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
+      })
+
+      it('Should fail with an unknown runner token', async function () {
+        await server.runners.unregister({ runnerToken: badUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
+      })
+    })
+
+    describe('Request', function () {
+
+      it('Should fail without a runner token', async function () {
+        await server.runnerJobs.request({ runnerToken: null, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
+      })
+
+      it('Should fail with a bad a runner token', async function () {
+        await server.runnerJobs.request({ runnerToken: '', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
+      })
+
+      it('Should fail with an unknown runner token', async function () {
+        await server.runnerJobs.request({ runnerToken: badUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
+      })
+    })
+
+    describe('Accept', function () {
+
+      it('Should fail with a bad a job uuid', async function () {
+        await server.runnerJobs.accept({ jobUUID: '', runnerToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
+      })
+
+      it('Should fail with an unknown job uuid', async function () {
+        await server.runnerJobs.accept({ jobUUID: badUUID, runnerToken, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
+      })
+
+      it('Should fail with a job not in pending state', async function () {
+        await server.runnerJobs.accept({ jobUUID: completedJobUUID, runnerToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
+        await server.runnerJobs.accept({ jobUUID: cancelledJobUUID, runnerToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
+      })
+
+      it('Should fail without a runner token', async function () {
+        await server.runnerJobs.accept({ jobUUID: pendingUUID, runnerToken: null, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
+      })
+
+      it('Should fail with a bad a runner token', async function () {
+        await server.runnerJobs.accept({ jobUUID: pendingUUID, runnerToken: '', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
+      })
+
+      it('Should fail with an unknown runner token', async function () {
+        await server.runnerJobs.accept({ jobUUID: pendingUUID, runnerToken: badUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
+      })
+    })
+
+    describe('Abort', function () {
+
+      it('Should fail without a reason', async function () {
+        await server.runnerJobs.abort({ jobUUID, jobToken, runnerToken, reason: null, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
+      })
+
+      it('Should fail with a bad reason', async function () {
+        const reason = 'reason'.repeat(5000)
+        await server.runnerJobs.abort({ jobUUID, jobToken, runnerToken, reason, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
+      })
+
+      it('Should fail with a job not in processing state', async function () {
+        await server.runnerJobs.abort({
+          jobUUID: completedJobUUID,
+          jobToken: completedJobToken,
+          runnerToken,
+          reason: 'reason',
+          expectedStatus: HttpStatusCode.BAD_REQUEST_400
+        })
+      })
+    })
+
+    describe('Update', function () {
+
+      describe('Common', function () {
+
+        it('Should fail with an invalid progress', async function () {
+          await server.runnerJobs.update({ jobUUID, jobToken, runnerToken, progress: 101, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
+        })
+
+        it('Should fail with a job not in processing state', async function () {
+          await server.runnerJobs.update({
+            jobUUID: completedJobUUID,
+            jobToken: completedJobToken,
+            runnerToken,
+            expectedStatus: HttpStatusCode.BAD_REQUEST_400
+          })
+        })
+      })
+
+      describe('Live RTMP to HLS', function () {
+        const base: RunnerJobUpdatePayload = {
+          masterPlaylistFile: 'live/master.m3u8',
+          resolutionPlaylistFilename: '0.m3u8',
+          resolutionPlaylistFile: 'live/1.m3u8',
+          type: 'add-chunk',
+          videoChunkFile: 'live/1-000069.ts',
+          videoChunkFilename: '1-000068.ts'
+        }
+
+        function testUpdate (payload: RunnerJobUpdatePayload) {
+          return server.runnerJobs.update({
+            jobUUID: liveAcceptedJob.uuid,
+            jobToken: liveAcceptedJob.jobToken,
+            payload,
+            runnerToken,
+            expectedStatus: HttpStatusCode.BAD_REQUEST_400
+          })
+        }
+
+        it('Should fail with an invalid resolutionPlaylistFilename', async function () {
+          await testUpdate({ ...base, resolutionPlaylistFilename: undefined })
+          await testUpdate({ ...base, resolutionPlaylistFilename: 'coucou/hello' })
+          await testUpdate({ ...base, resolutionPlaylistFilename: 'hello' })
+        })
+
+        it('Should fail with an invalid videoChunkFilename', async function () {
+          await testUpdate({ ...base, resolutionPlaylistFilename: undefined })
+          await testUpdate({ ...base, resolutionPlaylistFilename: 'coucou/hello' })
+          await testUpdate({ ...base, resolutionPlaylistFilename: 'hello' })
+        })
+
+        it('Should fail with an invalid type', async function () {
+          await testUpdate({ ...base, type: undefined })
+          await testUpdate({ ...base, type: 'toto' as any })
+        })
+
+        it('Should succeed with the correct params', async function () {
+          await server.runnerJobs.update({
+            jobUUID: liveAcceptedJob.uuid,
+            jobToken: liveAcceptedJob.jobToken,
+            payload: base,
+            runnerToken
+          })
+
+          await server.runnerJobs.update({
+            jobUUID: liveAcceptedJob.uuid,
+            jobToken: liveAcceptedJob.jobToken,
+            payload: { ...base, masterPlaylistFile: undefined },
+            runnerToken
+          })
+        })
+      })
+    })
+
+    describe('Error', function () {
+
+      it('Should fail with a missing error message', async function () {
+        await server.runnerJobs.error({ jobUUID, jobToken, runnerToken, message: null, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
+      })
+
+      it('Should fail with an invalid error messgae', async function () {
+        const message = 'a'.repeat(6000)
+        await server.runnerJobs.error({ jobUUID, jobToken, runnerToken, message, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
+      })
+
+      it('Should fail with a job not in processing state', async function () {
+        await server.runnerJobs.error({
+          jobUUID: completedJobUUID,
+          jobToken: completedJobToken,
+          message: 'my message',
+          runnerToken,
+          expectedStatus: HttpStatusCode.BAD_REQUEST_400
+        })
+      })
+    })
+
+    describe('Success', function () {
+      let vodJobUUID: string
+      let vodJobToken: string
+
+      describe('Common', function () {
+
+        it('Should fail with a job not in processing state', async function () {
+          await server.runnerJobs.success({
+            jobUUID: completedJobUUID,
+            jobToken: completedJobToken,
+            payload: { videoFile: 'video_short.mp4' },
+            runnerToken,
+            expectedStatus: HttpStatusCode.BAD_REQUEST_400
+          })
+        })
+      })
+
+      describe('VOD', function () {
+
+        it('Should fail with an invalid vod web video payload', async function () {
+          const { job } = await server.runnerJobs.autoAccept({ runnerToken, type: 'vod-web-video-transcoding' })
+
+          await server.runnerJobs.success({
+            jobUUID: job.uuid,
+            jobToken: job.jobToken,
+            payload: { hello: 'video_short.mp4' } as any,
+            runnerToken,
+            expectedStatus: HttpStatusCode.BAD_REQUEST_400
+          })
+
+          vodJobUUID = job.uuid
+          vodJobToken = job.jobToken
+        })
+
+        it('Should fail with an invalid vod hls payload', async function () {
+          // To create HLS jobs
+          const payload: RunnerJobSuccessPayload = { videoFile: 'video_short.mp4' }
+          await server.runnerJobs.success({ runnerToken, jobUUID: vodJobUUID, jobToken: vodJobToken, payload })
+
+          await waitJobs([ server ])
+
+          const { job } = await server.runnerJobs.autoAccept({ runnerToken, type: 'vod-hls-transcoding' })
+
+          await server.runnerJobs.success({
+            jobUUID: job.uuid,
+            jobToken: job.jobToken,
+            payload: { videoFile: 'video_short.mp4' } as any,
+            runnerToken,
+            expectedStatus: HttpStatusCode.BAD_REQUEST_400
+          })
+        })
+
+        it('Should fail with an invalid vod audio merge payload', async function () {
+          const attributes = { name: 'audio_with_preview', previewfile: 'preview.jpg', fixture: 'sample.ogg' }
+          await server.videos.upload({ attributes, mode: 'legacy' })
+
+          await waitJobs([ server ])
+
+          const { job } = await server.runnerJobs.autoAccept({ runnerToken, type: 'vod-audio-merge-transcoding' })
+
+          await server.runnerJobs.success({
+            jobUUID: job.uuid,
+            jobToken: job.jobToken,
+            payload: { hello: 'video_short.mp4' } as any,
+            runnerToken,
+            expectedStatus: HttpStatusCode.BAD_REQUEST_400
+          })
+        })
+      })
+    })
+
+    describe('Job files', function () {
+
+      describe('Video files', function () {
+
+        it('Should fail with an invalid video id', async function () {
+          await fetchFiles({ videoUUID: 'a', jobUUID, runnerToken, jobToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
+        })
+
+        it('Should fail with an unknown video id', async function () {
+          const videoUUID = '910ec12a-d9e6-458b-a274-0abb655f9464'
+          await fetchFiles({ videoUUID, jobUUID, runnerToken, jobToken, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
+        })
+
+        it('Should fail with a video id not associated to this job', async function () {
+          await fetchFiles({ videoUUID: videoUUID2, jobUUID, runnerToken, jobToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
+        })
+
+        it('Should succeed with the correct params', async function () {
+          await fetchFiles({ videoUUID, jobUUID, runnerToken, jobToken, expectedStatus: HttpStatusCode.OK_200 })
+        })
+      })
+    })
+  })
+
+  after(async function () {
+    await cleanupTests([ server ])
+  })
+})
index 4dc84d3f25e7e122d2f5dd3a3a67d8f9d60856be..8e9f615965758367ab2cfc614c7132fd4b99443a 100644 (file)
@@ -278,7 +278,7 @@ describe('Test video blacklist API validators', function () {
     })
 
     it('Should fail with an invalid type', async function () {
-      await servers[0].blacklist.list({ type: 0, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
+      await servers[0].blacklist.list({ type: 0 as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
     })
 
     it('Should succeed with the correct parameters', async function () {
index 6cb34c8a15d71df17a9b7ee537f32d3610a0c2e2..8090897c11ee330e9034096485eeca5c9887376e 100644 (file)
@@ -239,7 +239,7 @@ describe('Test video playlists API validator', function () {
     })
 
     it('Should fail with an incorrect privacy', async function () {
-      const params = getBase({ privacy: 45 })
+      const params = getBase({ privacy: 45 as any })
 
       await command.create(params)
       await command.update(getUpdate(params, playlist.shortUUID))
index 2a83143e2d663e56e32b182da537e8138f20d1fb..572ca8997507791bdfd6dfb60dd3b9f8b942c41f 100644 (file)
@@ -421,9 +421,9 @@ describe('Test videos API validator', function () {
         const error = body as unknown as PeerTubeProblemDocument
 
         if (mode === 'legacy') {
-          expect(error.docs).to.equal('https://docs.joinpeertube.org/api/rest-reference.html#operation/uploadLegacy')
+          expect(error.docs).to.equal('https://docs.joinpeertube.org/api-rest-reference.html#operation/uploadLegacy')
         } else {
-          expect(error.docs).to.equal('https://docs.joinpeertube.org/api/rest-reference.html#operation/uploadResumableInit')
+          expect(error.docs).to.equal('https://docs.joinpeertube.org/api-rest-reference.html#operation/uploadResumableInit')
         }
 
         expect(error.type).to.equal('about:blank')
@@ -680,7 +680,7 @@ describe('Test videos API validator', function () {
       const res = await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields })
       const error = res.body as PeerTubeProblemDocument
 
-      expect(error.docs).to.equal('https://docs.joinpeertube.org/api/rest-reference.html#operation/putVideo')
+      expect(error.docs).to.equal('https://docs.joinpeertube.org/api-rest-reference.html#operation/putVideo')
 
       expect(error.type).to.equal('about:blank')
       expect(error.title).to.equal('Bad Request')
@@ -729,7 +729,7 @@ describe('Test videos API validator', function () {
       const body = await server.videos.get({ id: 'hi', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
       const error = body as unknown as PeerTubeProblemDocument
 
-      expect(error.docs).to.equal('https://docs.joinpeertube.org/api/rest-reference.html#operation/getVideo')
+      expect(error.docs).to.equal('https://docs.joinpeertube.org/api-rest-reference.html#operation/getVideo')
 
       expect(error.type).to.equal('about:blank')
       expect(error.title).to.equal('Bad Request')
@@ -835,7 +835,7 @@ describe('Test videos API validator', function () {
       const body = await server.videos.remove({ id: 'hello', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
       const error = body as PeerTubeProblemDocument
 
-      expect(error.docs).to.equal('https://docs.joinpeertube.org/api/rest-reference.html#operation/delVideo')
+      expect(error.docs).to.equal('https://docs.joinpeertube.org/api-rest-reference.html#operation/delVideo')
 
       expect(error.type).to.equal('about:blank')
       expect(error.title).to.equal('Bad Request')
index 61352a134d2dc28ca291ac414587746b99b3aa8f..ef0c832944e0d3d45a21280b81b7febcddbbd6eb 100644 (file)
@@ -5,6 +5,7 @@ import './moderation'
 import './object-storage'
 import './notifications'
 import './redundancy'
+import './runners'
 import './search'
 import './server'
 import './transcoding'
index ceb606af1361a212a8a91a3921b7b09ae96188f7..f9b0d257bc846c9863663e6b8dbef143358afb13 100644 (file)
@@ -2,9 +2,9 @@
 
 import { expect } from 'chai'
 import { basename, join } from 'path'
-import { ffprobePromise, getVideoStream } from '@server/helpers/ffmpeg'
-import { testImage, testVideoResolutions } from '@server/tests/shared'
+import { SQLCommand, testImage, testLiveVideoResolutions } from '@server/tests/shared'
 import { getAllFiles, wait } from '@shared/core-utils'
+import { ffprobePromise, getVideoStream } from '@shared/ffmpeg'
 import {
   HttpStatusCode,
   LiveVideo,
@@ -365,6 +365,7 @@ describe('Test live', function () {
 
   describe('Live transcoding', function () {
     let liveVideoId: string
+    let sqlCommandServer1: SQLCommand
 
     async function createLiveWrapper (saveReplay: boolean) {
       const liveAttributes = {
@@ -407,6 +408,8 @@ describe('Test live', function () {
 
     before(async function () {
       await updateConf([])
+
+      sqlCommandServer1 = new SQLCommand(servers[0])
     })
 
     it('Should enable transcoding without additional resolutions', async function () {
@@ -418,8 +421,9 @@ describe('Test live', function () {
       await waitUntilLivePublishedOnAllServers(servers, liveVideoId)
       await waitJobs(servers)
 
-      await testVideoResolutions({
+      await testLiveVideoResolutions({
         originServer: servers[0],
+        sqlCommand: sqlCommandServer1,
         servers,
         liveVideoId,
         resolutions: [ 720 ],
@@ -453,8 +457,9 @@ describe('Test live', function () {
       await waitUntilLivePublishedOnAllServers(servers, liveVideoId)
       await waitJobs(servers)
 
-      await testVideoResolutions({
+      await testLiveVideoResolutions({
         originServer: servers[0],
+        sqlCommand: sqlCommandServer1,
         servers,
         liveVideoId,
         resolutions: resolutions.concat([ 720 ]),
@@ -505,8 +510,9 @@ describe('Test live', function () {
       await waitUntilLivePublishedOnAllServers(servers, liveVideoId)
       await waitJobs(servers)
 
-      await testVideoResolutions({
+      await testLiveVideoResolutions({
         originServer: servers[0],
+        sqlCommand: sqlCommandServer1,
         servers,
         liveVideoId,
         resolutions,
@@ -601,8 +607,9 @@ describe('Test live', function () {
       await waitUntilLivePublishedOnAllServers(servers, liveVideoId)
       await waitJobs(servers)
 
-      await testVideoResolutions({
+      await testLiveVideoResolutions({
         originServer: servers[0],
+        sqlCommand: sqlCommandServer1,
         servers,
         liveVideoId,
         resolutions,
@@ -637,8 +644,9 @@ describe('Test live', function () {
       await waitUntilLivePublishedOnAllServers(servers, liveVideoId)
       await waitJobs(servers)
 
-      await testVideoResolutions({
+      await testLiveVideoResolutions({
         originServer: servers[0],
+        sqlCommand: sqlCommandServer1,
         servers,
         liveVideoId,
         resolutions: [ 720 ],
@@ -661,6 +669,10 @@ describe('Test live', function () {
 
       expect(hlsFiles[0].resolution.id).to.equal(720)
     })
+
+    after(async function () {
+      await sqlCommandServer1.cleanup()
+    })
   })
 
   describe('After a server restart', function () {
index 6f059f622000994161251a2cbfe3824b686ba32d..4824542c97e3f855a0e11f99cb6e62d668043f71 100644 (file)
@@ -7,7 +7,8 @@ import {
   checkNewPluginVersion,
   MockJoinPeerTubeVersions,
   MockSmtpServer,
-  prepareNotificationsTest
+  prepareNotificationsTest,
+  SQLCommand
 } from '@server/tests/shared'
 import { wait } from '@shared/core-utils'
 import { PluginType, UserNotification, UserNotificationType } from '@shared/models'
@@ -15,6 +16,7 @@ import { cleanupTests, PeerTubeServer } from '@shared/server-commands'
 
 describe('Test admin notifications', function () {
   let server: PeerTubeServer
+  let sqlCommand: SQLCommand
   let userNotifications: UserNotification[] = []
   let adminNotifications: UserNotification[] = []
   let emails: object[] = []
@@ -58,6 +60,8 @@ describe('Test admin notifications', function () {
 
     await server.plugins.install({ npmName: 'peertube-plugin-hello-world' })
     await server.plugins.install({ npmName: 'peertube-theme-background-red' })
+
+    sqlCommand = new SQLCommand(server)
   })
 
   describe('Latest PeerTube version notification', function () {
@@ -116,8 +120,8 @@ describe('Test admin notifications', function () {
     it('Should send a notification to admins on new plugin version', async function () {
       this.timeout(30000)
 
-      await server.sql.setPluginVersion('hello-world', '0.0.1')
-      await server.sql.setPluginLatestVersion('hello-world', '0.0.1')
+      await sqlCommand.setPluginVersion('hello-world', '0.0.1')
+      await sqlCommand.setPluginLatestVersion('hello-world', '0.0.1')
       await wait(6000)
 
       await checkNewPluginVersion({ ...baseParams, pluginType: PluginType.PLUGIN, pluginName: 'hello-world', checkType: 'presence' })
@@ -138,8 +142,8 @@ describe('Test admin notifications', function () {
     it('Should send a new notification after a new plugin release', async function () {
       this.timeout(30000)
 
-      await server.sql.setPluginVersion('hello-world', '0.0.1')
-      await server.sql.setPluginLatestVersion('hello-world', '0.0.1')
+      await sqlCommand.setPluginVersion('hello-world', '0.0.1')
+      await sqlCommand.setPluginLatestVersion('hello-world', '0.0.1')
       await wait(6000)
 
       expect(adminNotifications.filter(n => n.type === UserNotificationType.NEW_PEERTUBE_VERSION)).to.have.lengthOf(2)
@@ -149,6 +153,7 @@ describe('Test admin notifications', function () {
   after(async function () {
     MockSmtpServer.Instance.kill()
 
+    await sqlCommand.cleanup()
     await cleanupTests([ server ])
   })
 })
index 588e0a8d7b81cc5c37f659612206d3eb79aadf07..c430cd0a066a6d70a516ebe0cbfad2b436a74daf 100644 (file)
@@ -1,7 +1,7 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
 import { expect } from 'chai'
-import { expectStartWith, MockObjectStorageProxy, testVideoResolutions } from '@server/tests/shared'
+import { expectStartWith, MockObjectStorageProxy, SQLCommand, testLiveVideoResolutions } from '@server/tests/shared'
 import { areMockObjectStorageTestsDisabled } from '@shared/core-utils'
 import { HttpStatusCode, LiveVideoCreate, VideoPrivacy } from '@shared/models'
 import {
@@ -79,6 +79,7 @@ describe('Object storage for lives', function () {
   if (areMockObjectStorageTestsDisabled()) return
 
   let servers: PeerTubeServer[]
+  let sqlCommandServer1: SQLCommand
 
   before(async function () {
     this.timeout(120000)
@@ -92,6 +93,8 @@ describe('Object storage for lives', function () {
     await doubleFollow(servers[0], servers[1])
 
     await servers[0].config.enableTranscoding()
+
+    sqlCommandServer1 = new SQLCommand(servers[0])
   })
 
   describe('Without live transcoding', function () {
@@ -109,8 +112,9 @@ describe('Object storage for lives', function () {
       const ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: videoUUID })
       await waitUntilLivePublishedOnAllServers(servers, videoUUID)
 
-      await testVideoResolutions({
+      await testLiveVideoResolutions({
         originServer: servers[0],
+        sqlCommand: sqlCommandServer1,
         servers,
         liveVideoId: videoUUID,
         resolutions: [ 720 ],
@@ -155,8 +159,9 @@ describe('Object storage for lives', function () {
         const ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: videoUUIDNonPermanent })
         await waitUntilLivePublishedOnAllServers(servers, videoUUIDNonPermanent)
 
-        await testVideoResolutions({
+        await testLiveVideoResolutions({
           originServer: servers[0],
+          sqlCommand: sqlCommandServer1,
           servers,
           liveVideoId: videoUUIDNonPermanent,
           resolutions,
@@ -194,8 +199,9 @@ describe('Object storage for lives', function () {
         const ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: videoUUIDPermanent })
         await waitUntilLivePublishedOnAllServers(servers, videoUUIDPermanent)
 
-        await testVideoResolutions({
+        await testLiveVideoResolutions({
           originServer: servers[0],
+          sqlCommand: sqlCommandServer1,
           servers,
           liveVideoId: videoUUIDPermanent,
           resolutions,
@@ -266,8 +272,9 @@ describe('Object storage for lives', function () {
       const ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: videoUUIDPermanent })
       await waitUntilLivePublishedOnAllServers(servers, videoUUIDPermanent)
 
-      await testVideoResolutions({
+      await testLiveVideoResolutions({
         originServer: servers[0],
+        sqlCommand: sqlCommandServer1,
         servers,
         liveVideoId: videoUUIDPermanent,
         resolutions: [ 720 ],
@@ -281,6 +288,8 @@ describe('Object storage for lives', function () {
   })
 
   after(async function () {
+    await sqlCommandServer1.cleanup()
+
     await killallServers(servers)
   })
 })
index 930c88543ba680a1e26273be084d226732e16103..af9d681b2a213475744c841868db28de1e5c483f 100644 (file)
@@ -2,7 +2,7 @@
 
 import { expect } from 'chai'
 import { basename } from 'path'
-import { checkVideoFileTokenReinjection, expectStartWith } from '@server/tests/shared'
+import { checkVideoFileTokenReinjection, expectStartWith, SQLCommand } from '@server/tests/shared'
 import { areScalewayObjectStorageTestsDisabled, getAllFiles, getHLS } from '@shared/core-utils'
 import { HttpStatusCode, LiveVideo, VideoDetails, VideoPrivacy } from '@shared/models'
 import {
@@ -30,6 +30,7 @@ describe('Object storage for video static file privacy', function () {
   if (areScalewayObjectStorageTestsDisabled()) return
 
   let server: PeerTubeServer
+  let sqlCommand: SQLCommand
   let userToken: string
 
   // ---------------------------------------------------------------------------
@@ -44,7 +45,7 @@ describe('Object storage for video static file privacy', function () {
     }
 
     for (const file of getAllFiles(video)) {
-      const internalFileUrl = await server.sql.getInternalFileUrl(file.id)
+      const internalFileUrl = await sqlCommand.getInternalFileUrl(file.id)
       expectStartWith(internalFileUrl, ObjectStorageCommand.getScalewayBaseUrl())
       await makeRawRequest({ url: internalFileUrl, token: server.accessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
     }
@@ -99,6 +100,8 @@ describe('Object storage for video static file privacy', function () {
     await server.config.enableMinimumTranscoding()
 
     userToken = await server.users.generateUserAndToken('user1')
+
+    sqlCommand = new SQLCommand(server)
   })
 
   describe('VOD', function () {
@@ -439,6 +442,7 @@ describe('Object storage for video static file privacy', function () {
       await server.servers.waitUntilLog('Removed files of video ' + v.url)
     }
 
+    await sqlCommand.cleanup()
     await cleanupTests([ server ])
   })
 })
index 6aaf32c345a35fee83be6ee4498903f34fde98da..e90753d09860f02cefcaaae05f17638f599e8e02 100644 (file)
@@ -6,12 +6,15 @@ import { stat } from 'fs-extra'
 import { merge } from 'lodash'
 import {
   checkTmpIsEmpty,
+  checkWebTorrentWorks,
   expectLogDoesNotContain,
   expectStartWith,
   generateHighBitrateVideo,
-  MockObjectStorageProxy
+  MockObjectStorageProxy,
+  SQLCommand
 } from '@server/tests/shared'
 import { areMockObjectStorageTestsDisabled } from '@shared/core-utils'
+import { sha1 } from '@shared/extra-utils'
 import { HttpStatusCode, VideoDetails } from '@shared/models'
 import {
   cleanupTests,
@@ -23,14 +26,13 @@ import {
   ObjectStorageCommand,
   PeerTubeServer,
   setAccessTokensToServers,
-  waitJobs,
-  webtorrentAdd
+  waitJobs
 } from '@shared/server-commands'
-import { sha1 } from '@shared/extra-utils'
 
 async function checkFiles (options: {
   server: PeerTubeServer
   originServer: PeerTubeServer
+  originSQLCommand: SQLCommand
 
   video: VideoDetails
 
@@ -45,6 +47,7 @@ async function checkFiles (options: {
   const {
     server,
     originServer,
+    originSQLCommand,
     video,
     playlistBucket,
     webtorrentBucket,
@@ -104,7 +107,7 @@ async function checkFiles (options: {
 
       if (originServer.internalServerNumber === server.internalServerNumber) {
         const infohash = sha1(`${2 + hls.playlistUrl}+V${i}`)
-        const dbInfohashes = await originServer.sql.getPlaylistInfohash(hls.id)
+        const dbInfohashes = await originSQLCommand.getPlaylistInfohash(hls.id)
 
         expect(dbInfohashes).to.include(infohash)
       }
@@ -114,11 +117,7 @@ async function checkFiles (options: {
   }
 
   for (const file of allFiles) {
-    const torrent = await webtorrentAdd(file.magnetUri, true)
-
-    expect(torrent.files).to.be.an('array')
-    expect(torrent.files.length).to.equal(1)
-    expect(torrent.files[0].path).to.exist.and.to.not.equal('')
+    await checkWebTorrentWorks(file.magnetUri)
 
     const res = await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 })
     expect(res.body).to.have.length.above(100)
@@ -145,6 +144,7 @@ function runTestSuite (options: {
   let baseMockUrl: string
 
   let servers: PeerTubeServer[]
+  let sqlCommands: SQLCommand[]
 
   let keptUrls: string[] = []
 
@@ -202,6 +202,8 @@ function runTestSuite (options: {
       const files = await server.videos.listFiles({ id: uuid })
       keptUrls = keptUrls.concat(files.map(f => f.fileUrl))
     }
+
+    sqlCommands = servers.map(s => new SQLCommand(s))
   })
 
   it('Should upload a video and move it to the object storage without transcoding', async function () {
@@ -214,7 +216,7 @@ function runTestSuite (options: {
 
     for (const server of servers) {
       const video = await server.videos.get({ id: uuid })
-      const files = await checkFiles({ ...options, server, originServer: servers[0], video, baseMockUrl })
+      const files = await checkFiles({ ...options, server, originServer: servers[0], originSQLCommand: sqlCommands[0], video, baseMockUrl })
 
       deletedUrls = deletedUrls.concat(files)
     }
@@ -230,7 +232,7 @@ function runTestSuite (options: {
 
     for (const server of servers) {
       const video = await server.videos.get({ id: uuid })
-      const files = await checkFiles({ ...options, server, originServer: servers[0], video, baseMockUrl })
+      const files = await checkFiles({ ...options, server, originServer: servers[0], originSQLCommand: sqlCommands[0], video, baseMockUrl })
 
       deletedUrls = deletedUrls.concat(files)
     }
@@ -274,6 +276,10 @@ function runTestSuite (options: {
   after(async function () {
     await mockObjectStorageProxy.terminate()
 
+    for (const sqlCommand of sqlCommands) {
+      await sqlCommand.cleanup()
+    }
+
     await cleanupTests(servers)
   })
 }
diff --git a/server/tests/api/runners/index.ts b/server/tests/api/runners/index.ts
new file mode 100644 (file)
index 0000000..7f33ec8
--- /dev/null
@@ -0,0 +1,4 @@
+export * from './runner-common'
+export * from './runner-live-transcoding'
+export * from './runner-socket'
+export * from './runner-vod-transcoding'
diff --git a/server/tests/api/runners/runner-common.ts b/server/tests/api/runners/runner-common.ts
new file mode 100644 (file)
index 0000000..a220475
--- /dev/null
@@ -0,0 +1,662 @@
+/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
+
+import { expect } from 'chai'
+import { wait } from '@shared/core-utils'
+import { HttpStatusCode, Runner, RunnerJob, RunnerJobAdmin, RunnerJobState, RunnerRegistrationToken } from '@shared/models'
+import {
+  cleanupTests,
+  createSingleServer,
+  makePostBodyRequest,
+  PeerTubeServer,
+  setAccessTokensToServers,
+  setDefaultVideoChannel,
+  waitJobs
+} from '@shared/server-commands'
+
+describe('Test runner common actions', function () {
+  let server: PeerTubeServer
+  let registrationToken: string
+  let runnerToken: string
+  let jobMaxPriority: string
+
+  before(async function () {
+    this.timeout(120_000)
+
+    server = await createSingleServer(1, {
+      remote_runners: {
+        stalled_jobs: {
+          vod: '5 seconds'
+        }
+      }
+    })
+
+    await setAccessTokensToServers([ server ])
+    await setDefaultVideoChannel([ server ])
+
+    await server.config.enableTranscoding(true, true)
+    await server.config.enableRemoteTranscoding()
+  })
+
+  describe('Managing runner registration tokens', function () {
+    let base: RunnerRegistrationToken[]
+    let registrationTokenToDelete: RunnerRegistrationToken
+
+    it('Should have a default registration token', async function () {
+      const { total, data } = await server.runnerRegistrationTokens.list()
+
+      expect(total).to.equal(1)
+      expect(data).to.have.lengthOf(1)
+
+      const token = data[0]
+      expect(token.id).to.exist
+      expect(token.createdAt).to.exist
+      expect(token.updatedAt).to.exist
+      expect(token.registeredRunnersCount).to.equal(0)
+      expect(token.registrationToken).to.exist
+    })
+
+    it('Should create other registration tokens', async function () {
+      await server.runnerRegistrationTokens.generate()
+      await server.runnerRegistrationTokens.generate()
+
+      const { total, data } = await server.runnerRegistrationTokens.list()
+      expect(total).to.equal(3)
+      expect(data).to.have.lengthOf(3)
+    })
+
+    it('Should list registration tokens', async function () {
+      {
+        const { total, data } = await server.runnerRegistrationTokens.list({ sort: 'createdAt' })
+        expect(total).to.equal(3)
+        expect(data).to.have.lengthOf(3)
+        expect(new Date(data[0].createdAt)).to.be.below(new Date(data[1].createdAt))
+        expect(new Date(data[1].createdAt)).to.be.below(new Date(data[2].createdAt))
+
+        base = data
+
+        registrationTokenToDelete = data[0]
+        registrationToken = data[1].registrationToken
+      }
+
+      {
+        const { total, data } = await server.runnerRegistrationTokens.list({ sort: '-createdAt', start: 2, count: 1 })
+        expect(total).to.equal(3)
+        expect(data).to.have.lengthOf(1)
+        expect(data[0].registrationToken).to.equal(base[0].registrationToken)
+      }
+    })
+
+    it('Should have appropriate registeredRunnersCount for registration tokens', async function () {
+      await server.runners.register({ name: 'to delete 1', registrationToken: registrationTokenToDelete.registrationToken })
+      await server.runners.register({ name: 'to delete 2', registrationToken: registrationTokenToDelete.registrationToken })
+
+      const { data } = await server.runnerRegistrationTokens.list()
+
+      for (const d of data) {
+        if (d.registrationToken === registrationTokenToDelete.registrationToken) {
+          expect(d.registeredRunnersCount).to.equal(2)
+        } else {
+          expect(d.registeredRunnersCount).to.equal(0)
+        }
+      }
+
+      const { data: runners } = await server.runners.list()
+      expect(runners).to.have.lengthOf(2)
+    })
+
+    it('Should delete a registration token', async function () {
+      await server.runnerRegistrationTokens.delete({ id: registrationTokenToDelete.id })
+
+      const { total, data } = await server.runnerRegistrationTokens.list({ sort: 'createdAt' })
+      expect(total).to.equal(2)
+      expect(data).to.have.lengthOf(2)
+
+      for (const d of data) {
+        expect(d.registeredRunnersCount).to.equal(0)
+        expect(d.registrationToken).to.not.equal(registrationTokenToDelete.registrationToken)
+      }
+    })
+
+    it('Should have removed runners of this registration token', async function () {
+      const { data: runners } = await server.runners.list()
+      expect(runners).to.have.lengthOf(0)
+    })
+  })
+
+  describe('Managing runners', function () {
+    let toDelete: Runner
+
+    it('Should not have runners available', async function () {
+      const { total, data } = await server.runners.list()
+
+      expect(data).to.have.lengthOf(0)
+      expect(total).to.equal(0)
+    })
+
+    it('Should register runners', async function () {
+      const now = new Date()
+
+      const result = await server.runners.register({
+        name: 'runner 1',
+        description: 'my super runner 1',
+        registrationToken
+      })
+      expect(result.runnerToken).to.exist
+      runnerToken = result.runnerToken
+
+      await server.runners.register({
+        name: 'runner 2',
+        registrationToken
+      })
+
+      const { total, data } = await server.runners.list({ sort: 'createdAt' })
+      expect(total).to.equal(2)
+      expect(data).to.have.lengthOf(2)
+
+      for (const d of data) {
+        expect(d.id).to.exist
+        expect(d.createdAt).to.exist
+        expect(d.updatedAt).to.exist
+        expect(new Date(d.createdAt)).to.be.above(now)
+        expect(new Date(d.updatedAt)).to.be.above(now)
+        expect(new Date(d.lastContact)).to.be.above(now)
+        expect(d.ip).to.exist
+      }
+
+      expect(data[0].name).to.equal('runner 1')
+      expect(data[0].description).to.equal('my super runner 1')
+
+      expect(data[1].name).to.equal('runner 2')
+      expect(data[1].description).to.be.null
+
+      toDelete = data[1]
+    })
+
+    it('Should list runners', async function () {
+      const { total, data } = await server.runners.list({ sort: '-createdAt', start: 1, count: 1 })
+
+      expect(total).to.equal(2)
+      expect(data).to.have.lengthOf(1)
+      expect(data[0].name).to.equal('runner 1')
+    })
+
+    it('Should delete a runner', async function () {
+      await server.runners.delete({ id: toDelete.id })
+
+      const { total, data } = await server.runners.list()
+
+      expect(total).to.equal(1)
+      expect(data).to.have.lengthOf(1)
+      expect(data[0].name).to.equal('runner 1')
+    })
+
+    it('Should unregister a runner', async function () {
+      const registered = await server.runners.autoRegisterRunner()
+
+      {
+        const { total, data } = await server.runners.list()
+        expect(total).to.equal(2)
+        expect(data).to.have.lengthOf(2)
+      }
+
+      await server.runners.unregister({ runnerToken: registered })
+
+      {
+        const { total, data } = await server.runners.list()
+        expect(total).to.equal(1)
+        expect(data).to.have.lengthOf(1)
+        expect(data[0].name).to.equal('runner 1')
+      }
+    })
+  })
+
+  describe('Managing runner jobs', function () {
+    let jobUUID: string
+    let jobToken: string
+    let lastRunnerContact: Date
+    let failedJob: RunnerJob
+
+    async function checkMainJobState (
+      mainJobState: RunnerJobState,
+      otherJobStates: RunnerJobState[] = [ RunnerJobState.PENDING, RunnerJobState.WAITING_FOR_PARENT_JOB ]
+    ) {
+      const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' })
+
+      for (const job of data) {
+        if (job.uuid === jobUUID) {
+          expect(job.state.id).to.equal(mainJobState)
+        } else {
+          expect(otherJobStates).to.include(job.state.id)
+        }
+      }
+    }
+
+    function getMainJob () {
+      return server.runnerJobs.getJob({ uuid: jobUUID })
+    }
+
+    describe('List jobs', function () {
+
+      it('Should not have jobs', async function () {
+        const { total, data } = await server.runnerJobs.list()
+
+        expect(data).to.have.lengthOf(0)
+        expect(total).to.equal(0)
+      })
+
+      it('Should upload a video and have available jobs', async function () {
+        await server.videos.quickUpload({ name: 'to transcode' })
+        await waitJobs([ server ])
+
+        const { total, data } = await server.runnerJobs.list()
+
+        expect(data).to.have.lengthOf(10)
+        expect(total).to.equal(10)
+
+        for (const job of data) {
+          expect(job.startedAt).to.not.exist
+          expect(job.finishedAt).to.not.exist
+          expect(job.payload).to.exist
+          expect(job.privatePayload).to.exist
+        }
+
+        const hlsJobs = data.filter(d => d.type === 'vod-hls-transcoding')
+        const webVideoJobs = data.filter(d => d.type === 'vod-web-video-transcoding')
+
+        expect(hlsJobs).to.have.lengthOf(5)
+        expect(webVideoJobs).to.have.lengthOf(5)
+
+        const pendingJobs = data.filter(d => d.state.id === RunnerJobState.PENDING)
+        const waitingJobs = data.filter(d => d.state.id === RunnerJobState.WAITING_FOR_PARENT_JOB)
+
+        expect(pendingJobs).to.have.lengthOf(1)
+        expect(waitingJobs).to.have.lengthOf(9)
+      })
+
+      it('Should upload another video and list/sort jobs', async function () {
+        await server.videos.quickUpload({ name: 'to transcode 2' })
+        await waitJobs([ server ])
+
+        {
+          const { total, data } = await server.runnerJobs.list({ start: 0, count: 30 })
+
+          expect(data).to.have.lengthOf(20)
+          expect(total).to.equal(20)
+
+          jobUUID = data[16].uuid
+        }
+
+        {
+          const { total, data } = await server.runnerJobs.list({ start: 3, count: 1, sort: 'createdAt' })
+          expect(total).to.equal(20)
+
+          expect(data).to.have.lengthOf(1)
+          expect(data[0].uuid).to.equal(jobUUID)
+        }
+
+        {
+          let previousPriority = Infinity
+          const { total, data } = await server.runnerJobs.list({ start: 0, count: 100, sort: '-priority' })
+          expect(total).to.equal(20)
+
+          for (const job of data) {
+            expect(job.priority).to.be.at.most(previousPriority)
+            previousPriority = job.priority
+
+            if (job.state.id === RunnerJobState.PENDING) {
+              jobMaxPriority = job.uuid
+            }
+          }
+        }
+      })
+
+      it('Should search jobs', async function () {
+        {
+          const { total, data } = await server.runnerJobs.list({ search: jobUUID })
+
+          expect(data).to.have.lengthOf(1)
+          expect(total).to.equal(1)
+
+          expect(data[0].uuid).to.equal(jobUUID)
+        }
+
+        {
+          const { total, data } = await server.runnerJobs.list({ search: 'toto' })
+
+          expect(data).to.have.lengthOf(0)
+          expect(total).to.equal(0)
+        }
+
+        {
+          const { total, data } = await server.runnerJobs.list({ search: 'hls' })
+
+          expect(data).to.not.have.lengthOf(0)
+          expect(total).to.not.equal(0)
+        }
+      })
+    })
+
+    describe('Accept/update/abort/process a job', function () {
+
+      it('Should request available jobs', async function () {
+        lastRunnerContact = new Date()
+
+        const { availableJobs } = await server.runnerJobs.request({ runnerToken })
+
+        // Only optimize jobs are available
+        expect(availableJobs).to.have.lengthOf(2)
+
+        for (const job of availableJobs) {
+          expect(job.uuid).to.exist
+          expect(job.payload.input).to.exist
+          expect(job.payload.output).to.exist
+
+          expect((job as RunnerJobAdmin).privatePayload).to.not.exist
+        }
+
+        const hlsJobs = availableJobs.filter(d => d.type === 'vod-hls-transcoding')
+        const webVideoJobs = availableJobs.filter(d => d.type === 'vod-web-video-transcoding')
+
+        expect(hlsJobs).to.have.lengthOf(0)
+        expect(webVideoJobs).to.have.lengthOf(2)
+
+        jobUUID = webVideoJobs[0].uuid
+      })
+
+      it('Should have sorted available jobs by priority', async function () {
+        const { availableJobs } = await server.runnerJobs.request({ runnerToken })
+
+        expect(availableJobs[0].uuid).to.equal(jobMaxPriority)
+      })
+
+      it('Should have last runner contact updated', async function () {
+        await wait(1000)
+
+        const { data } = await server.runners.list({ sort: 'createdAt' })
+        expect(new Date(data[0].lastContact)).to.be.above(lastRunnerContact)
+      })
+
+      it('Should accept a job', async function () {
+        const startedAt = new Date()
+
+        const { job } = await server.runnerJobs.accept({ runnerToken, jobUUID })
+        jobToken = job.jobToken
+
+        const checkProcessingJob = (job: RunnerJob & { jobToken?: string }, fromAccept: boolean) => {
+          expect(job.uuid).to.equal(jobUUID)
+
+          expect(job.type).to.equal('vod-web-video-transcoding')
+          expect(job.state.label).to.equal('Processing')
+          expect(job.state.id).to.equal(RunnerJobState.PROCESSING)
+
+          expect(job.runner).to.exist
+          expect(job.runner.name).to.equal('runner 1')
+          expect(job.runner.description).to.equal('my super runner 1')
+
+          expect(job.progress).to.be.null
+
+          expect(job.startedAt).to.exist
+          expect(new Date(job.startedAt)).to.be.above(startedAt)
+
+          expect(job.finishedAt).to.not.exist
+
+          expect(job.failures).to.equal(0)
+
+          expect(job.payload).to.exist
+
+          if (fromAccept) {
+            expect(job.jobToken).to.exist
+            expect((job as RunnerJobAdmin).privatePayload).to.not.exist
+          } else {
+            expect(job.jobToken).to.not.exist
+            expect((job as RunnerJobAdmin).privatePayload).to.exist
+          }
+        }
+
+        checkProcessingJob(job, true)
+
+        const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' })
+
+        const processingJob = data.find(j => j.uuid === jobUUID)
+        checkProcessingJob(processingJob, false)
+
+        await checkMainJobState(RunnerJobState.PROCESSING)
+      })
+
+      it('Should update a job', async function () {
+        await server.runnerJobs.update({ runnerToken, jobUUID, jobToken, progress: 53 })
+
+        const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' })
+
+        for (const job of data) {
+          if (job.state.id === RunnerJobState.PROCESSING) {
+            expect(job.progress).to.equal(53)
+          } else {
+            expect(job.progress).to.be.null
+          }
+        }
+      })
+
+      it('Should abort a job', async function () {
+        await server.runnerJobs.abort({ runnerToken, jobUUID, jobToken, reason: 'for tests' })
+
+        await checkMainJobState(RunnerJobState.PENDING)
+
+        const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' })
+        for (const job of data) {
+          expect(job.progress).to.be.null
+        }
+      })
+
+      it('Should accept the same job again and post a success', async function () {
+        const { availableJobs } = await server.runnerJobs.request({ runnerToken })
+        expect(availableJobs.find(j => j.uuid === jobUUID)).to.exist
+
+        const { job } = await server.runnerJobs.accept({ runnerToken, jobUUID })
+        jobToken = job.jobToken
+
+        await checkMainJobState(RunnerJobState.PROCESSING)
+
+        const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' })
+
+        for (const job of data) {
+          expect(job.progress).to.be.null
+        }
+
+        const payload = {
+          videoFile: 'video_short.mp4'
+        }
+
+        await server.runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
+      })
+
+      it('Should not have available jobs anymore', async function () {
+        await checkMainJobState(RunnerJobState.COMPLETED)
+
+        const job = await getMainJob()
+        expect(job.finishedAt).to.exist
+
+        const { availableJobs } = await server.runnerJobs.request({ runnerToken })
+        expect(availableJobs.find(j => j.uuid === jobUUID)).to.not.exist
+      })
+    })
+
+    describe('Error job', function () {
+
+      it('Should accept another job and post an error', async function () {
+        await server.runnerJobs.cancelAllJobs()
+        await server.videos.quickUpload({ name: 'video' })
+        await waitJobs([ server ])
+
+        const { availableJobs } = await server.runnerJobs.request({ runnerToken })
+        jobUUID = availableJobs[0].uuid
+
+        const { job } = await server.runnerJobs.accept({ runnerToken, jobUUID })
+        jobToken = job.jobToken
+
+        await server.runnerJobs.error({ runnerToken, jobUUID, jobToken, message: 'Error' })
+      })
+
+      it('Should have job failures increased', async function () {
+        const job = await getMainJob()
+        expect(job.state.id).to.equal(RunnerJobState.PENDING)
+        expect(job.failures).to.equal(1)
+        expect(job.error).to.be.null
+        expect(job.progress).to.be.null
+        expect(job.finishedAt).to.not.exist
+      })
+
+      it('Should error a job when job attempts is too big', async function () {
+        for (let i = 0; i < 4; i++) {
+          const { job } = await server.runnerJobs.accept({ runnerToken, jobUUID })
+          jobToken = job.jobToken
+
+          await server.runnerJobs.error({ runnerToken, jobUUID, jobToken, message: 'Error ' + i })
+        }
+
+        const job = await getMainJob()
+        expect(job.failures).to.equal(5)
+        expect(job.state.id).to.equal(RunnerJobState.ERRORED)
+        expect(job.state.label).to.equal('Errored')
+        expect(job.error).to.equal('Error 3')
+        expect(job.progress).to.be.null
+        expect(job.finishedAt).to.exist
+
+        failedJob = job
+      })
+
+      it('Should have failed children jobs too', async function () {
+        const { data } = await server.runnerJobs.list({ count: 50, sort: '-updatedAt' })
+
+        const children = data.filter(j => j.parent?.uuid === failedJob.uuid)
+        expect(children).to.have.lengthOf(9)
+
+        for (const child of children) {
+          expect(child.parent.uuid).to.equal(failedJob.uuid)
+          expect(child.parent.type).to.equal(failedJob.type)
+          expect(child.parent.state.id).to.equal(failedJob.state.id)
+          expect(child.parent.state.label).to.equal(failedJob.state.label)
+
+          expect(child.state.id).to.equal(RunnerJobState.PARENT_ERRORED)
+          expect(child.state.label).to.equal('Parent job failed')
+        }
+      })
+    })
+
+    describe('Cancel', function () {
+
+      it('Should cancel a pending job', async function () {
+        await server.videos.quickUpload({ name: 'video' })
+        await waitJobs([ server ])
+
+        {
+          const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' })
+
+          const pendingJob = data.find(j => j.state.id === RunnerJobState.PENDING)
+          jobUUID = pendingJob.uuid
+
+          await server.runnerJobs.cancelByAdmin({ jobUUID })
+        }
+
+        {
+          const job = await getMainJob()
+          expect(job.state.id).to.equal(RunnerJobState.CANCELLED)
+          expect(job.state.label).to.equal('Cancelled')
+        }
+
+        {
+          const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' })
+          const children = data.filter(j => j.parent?.uuid === jobUUID)
+          expect(children).to.have.lengthOf(9)
+
+          for (const child of children) {
+            expect(child.state.id).to.equal(RunnerJobState.PARENT_CANCELLED)
+          }
+        }
+      })
+
+      it('Should cancel an already accepted job and skip success/error', async function () {
+        await server.videos.quickUpload({ name: 'video' })
+        await waitJobs([ server ])
+
+        const { availableJobs } = await server.runnerJobs.request({ runnerToken })
+        jobUUID = availableJobs[0].uuid
+
+        const { job } = await server.runnerJobs.accept({ runnerToken, jobUUID })
+        jobToken = job.jobToken
+
+        await server.runnerJobs.cancelByAdmin({ jobUUID })
+
+        await server.runnerJobs.abort({ runnerToken, jobUUID, jobToken, reason: 'aborted', expectedStatus: HttpStatusCode.NOT_FOUND_404 })
+      })
+    })
+
+    describe('Stalled jobs', function () {
+
+      it('Should abort stalled jobs', async function () {
+        this.timeout(60000)
+
+        await server.videos.quickUpload({ name: 'video' })
+        await server.videos.quickUpload({ name: 'video' })
+        await waitJobs([ server ])
+
+        const { job: job1 } = await server.runnerJobs.autoAccept({ runnerToken })
+        const { job: stalledJob } = await server.runnerJobs.autoAccept({ runnerToken })
+
+        for (let i = 0; i < 6; i++) {
+          await wait(2000)
+
+          await server.runnerJobs.update({ runnerToken, jobToken: job1.jobToken, jobUUID: job1.uuid })
+        }
+
+        const refreshedJob1 = await server.runnerJobs.getJob({ uuid: job1.uuid })
+        const refreshedStalledJob = await server.runnerJobs.getJob({ uuid: stalledJob.uuid })
+
+        expect(refreshedJob1.state.id).to.equal(RunnerJobState.PROCESSING)
+        expect(refreshedStalledJob.state.id).to.equal(RunnerJobState.PENDING)
+      })
+    })
+
+    describe('Rate limit', function () {
+
+      before(async function () {
+        this.timeout(60000)
+
+        await server.kill()
+
+        await server.run({
+          rates_limit: {
+            api: {
+              max: 10
+            }
+          }
+        })
+      })
+
+      it('Should rate limit an unknown runner', async function () {
+        const path = '/api/v1/ping'
+        const fields = { runnerToken: 'toto' }
+
+        for (let i = 0; i < 20; i++) {
+          try {
+            await makePostBodyRequest({ url: server.url, path, fields, expectedStatus: HttpStatusCode.OK_200 })
+          } catch {}
+        }
+
+        await makePostBodyRequest({ url: server.url, path, fields, expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429 })
+      })
+
+      it('Should not rate limit a registered runner', async function () {
+        const path = '/api/v1/ping'
+
+        for (let i = 0; i < 20; i++) {
+          await makePostBodyRequest({ url: server.url, path, fields: { runnerToken }, expectedStatus: HttpStatusCode.OK_200 })
+        }
+      })
+    })
+  })
+
+  after(async function () {
+    await cleanupTests([ server ])
+  })
+})
diff --git a/server/tests/api/runners/runner-live-transcoding.ts b/server/tests/api/runners/runner-live-transcoding.ts
new file mode 100644 (file)
index 0000000..b11d540
--- /dev/null
@@ -0,0 +1,330 @@
+/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
+
+import { expect } from 'chai'
+import { FfmpegCommand } from 'fluent-ffmpeg'
+import { readFile } from 'fs-extra'
+import { buildAbsoluteFixturePath, wait } from '@shared/core-utils'
+import {
+  HttpStatusCode,
+  LiveRTMPHLSTranscodingUpdatePayload,
+  LiveVideo,
+  LiveVideoError,
+  RunnerJob,
+  RunnerJobLiveRTMPHLSTranscodingPayload,
+  Video,
+  VideoPrivacy,
+  VideoState
+} from '@shared/models'
+import {
+  cleanupTests,
+  createSingleServer,
+  makeRawRequest,
+  PeerTubeServer,
+  sendRTMPStream,
+  setAccessTokensToServers,
+  setDefaultVideoChannel,
+  stopFfmpeg,
+  testFfmpegStreamError,
+  waitJobs
+} from '@shared/server-commands'
+
+describe('Test runner live transcoding', function () {
+  let server: PeerTubeServer
+  let runnerToken: string
+  let baseUrl: string
+
+  before(async function () {
+    this.timeout(120_000)
+
+    server = await createSingleServer(1)
+
+    await setAccessTokensToServers([ server ])
+    await setDefaultVideoChannel([ server ])
+
+    await server.config.enableRemoteTranscoding()
+    await server.config.enableTranscoding()
+    runnerToken = await server.runners.autoRegisterRunner()
+
+    baseUrl = server.url + '/static/streaming-playlists/hls'
+  })
+
+  describe('Without transcoding enabled', function () {
+
+    before(async function () {
+      await server.config.enableLive({
+        allowReplay: false,
+        resolutions: 'min',
+        transcoding: false
+      })
+    })
+
+    it('Should not have available jobs', async function () {
+      this.timeout(120000)
+
+      const { live, video } = await server.live.quickCreate({ permanentLive: true, saveReplay: false, privacy: VideoPrivacy.PUBLIC })
+
+      const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey })
+      await server.live.waitUntilPublished({ videoId: video.id })
+
+      await waitJobs([ server ])
+
+      const { availableJobs } = await server.runnerJobs.requestLive({ runnerToken })
+      expect(availableJobs).to.have.lengthOf(0)
+
+      await stopFfmpeg(ffmpegCommand)
+    })
+  })
+
+  describe('With transcoding enabled on classic live', function () {
+    let live: LiveVideo
+    let video: Video
+    let ffmpegCommand: FfmpegCommand
+    let jobUUID: string
+    let acceptedJob: RunnerJob & { jobToken: string }
+
+    async function testPlaylistFile (fixture: string, expected: string) {
+      const text = await server.streamingPlaylists.get({ url: `${baseUrl}/${video.uuid}/${fixture}` })
+      expect(await readFile(buildAbsoluteFixturePath(expected), 'utf-8')).to.equal(text)
+
+    }
+
+    async function testTSFile (fixture: string, expected: string) {
+      const { body } = await makeRawRequest({ url: `${baseUrl}/${video.uuid}/${fixture}`, expectedStatus: HttpStatusCode.OK_200 })
+      expect(await readFile(buildAbsoluteFixturePath(expected))).to.deep.equal(body)
+    }
+
+    before(async function () {
+      await server.config.enableLive({
+        allowReplay: true,
+        resolutions: 'max',
+        transcoding: true
+      })
+    })
+
+    it('Should publish a a live and have available jobs', async function () {
+      this.timeout(120000)
+
+      const data = await server.live.quickCreate({ permanentLive: false, saveReplay: false, privacy: VideoPrivacy.PUBLIC })
+      live = data.live
+      video = data.video
+
+      ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey })
+      await waitJobs([ server ])
+
+      const job = await server.runnerJobs.requestLiveJob(runnerToken)
+      jobUUID = job.uuid
+
+      expect(job.type).to.equal('live-rtmp-hls-transcoding')
+      expect(job.payload.input.rtmpUrl).to.exist
+
+      expect(job.payload.output.toTranscode).to.have.lengthOf(5)
+
+      for (const { resolution, fps } of job.payload.output.toTranscode) {
+        expect([ 720, 480, 360, 240, 144 ]).to.contain(resolution)
+
+        expect(fps).to.be.above(25)
+        expect(fps).to.be.below(70)
+      }
+    })
+
+    it('Should update the live with a new chunk', async function () {
+      this.timeout(120000)
+
+      const { job } = await server.runnerJobs.accept<RunnerJobLiveRTMPHLSTranscodingPayload>({ jobUUID, runnerToken })
+      acceptedJob = job
+
+      {
+        const payload: LiveRTMPHLSTranscodingUpdatePayload = {
+          masterPlaylistFile: 'live/master.m3u8',
+          resolutionPlaylistFile: 'live/0.m3u8',
+          resolutionPlaylistFilename: '0.m3u8',
+          type: 'add-chunk',
+          videoChunkFile: 'live/0-000067.ts',
+          videoChunkFilename: '0-000067.ts'
+        }
+        await server.runnerJobs.update({ jobUUID, runnerToken, jobToken: job.jobToken, payload, progress: 50 })
+
+        const updatedJob = await server.runnerJobs.getJob({ uuid: job.uuid })
+        expect(updatedJob.progress).to.equal(50)
+      }
+
+      {
+        const payload: LiveRTMPHLSTranscodingUpdatePayload = {
+          resolutionPlaylistFile: 'live/1.m3u8',
+          resolutionPlaylistFilename: '1.m3u8',
+          type: 'add-chunk',
+          videoChunkFile: 'live/1-000068.ts',
+          videoChunkFilename: '1-000068.ts'
+        }
+        await server.runnerJobs.update({ jobUUID, runnerToken, jobToken: job.jobToken, payload })
+      }
+
+      await wait(1000)
+
+      await testPlaylistFile('master.m3u8', 'live/master.m3u8')
+      await testPlaylistFile('0.m3u8', 'live/0.m3u8')
+      await testPlaylistFile('1.m3u8', 'live/1.m3u8')
+
+      await testTSFile('0-000067.ts', 'live/0-000067.ts')
+      await testTSFile('1-000068.ts', 'live/1-000068.ts')
+    })
+
+    it('Should replace existing m3u8 on update', async function () {
+      this.timeout(120000)
+
+      const payload: LiveRTMPHLSTranscodingUpdatePayload = {
+        masterPlaylistFile: 'live/1.m3u8',
+        resolutionPlaylistFilename: '0.m3u8',
+        resolutionPlaylistFile: 'live/1.m3u8',
+        type: 'add-chunk',
+        videoChunkFile: 'live/1-000069.ts',
+        videoChunkFilename: '1-000068.ts'
+      }
+      await server.runnerJobs.update({ jobUUID, runnerToken, jobToken: acceptedJob.jobToken, payload })
+      await wait(1000)
+
+      await testPlaylistFile('master.m3u8', 'live/1.m3u8')
+      await testPlaylistFile('0.m3u8', 'live/1.m3u8')
+      await testTSFile('1-000068.ts', 'live/1-000069.ts')
+    })
+
+    it('Should update the live with removed chunks', async function () {
+      this.timeout(120000)
+
+      const payload: LiveRTMPHLSTranscodingUpdatePayload = {
+        resolutionPlaylistFile: 'live/0.m3u8',
+        resolutionPlaylistFilename: '0.m3u8',
+        type: 'remove-chunk',
+        videoChunkFilename: '1-000068.ts'
+      }
+      await server.runnerJobs.update({ jobUUID, runnerToken, jobToken: acceptedJob.jobToken, payload })
+
+      await wait(1000)
+
+      await server.streamingPlaylists.get({ url: `${baseUrl}/${video.uuid}/master.m3u8` })
+      await server.streamingPlaylists.get({ url: `${baseUrl}/${video.uuid}/0.m3u8` })
+      await server.streamingPlaylists.get({ url: `${baseUrl}/${video.uuid}/1.m3u8` })
+      await makeRawRequest({ url: `${baseUrl}/${video.uuid}/0-000067.ts`, expectedStatus: HttpStatusCode.OK_200 })
+      await makeRawRequest({ url: `${baseUrl}/${video.uuid}/1-000068.ts`, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
+    })
+
+    it('Should complete the live and save the replay', async function () {
+      this.timeout(120000)
+
+      for (const segment of [ '0-000069.ts', '0-000070.ts' ]) {
+        const payload: LiveRTMPHLSTranscodingUpdatePayload = {
+          masterPlaylistFile: 'live/master.m3u8',
+          resolutionPlaylistFilename: '0.m3u8',
+          resolutionPlaylistFile: 'live/0.m3u8',
+          type: 'add-chunk',
+          videoChunkFile: 'live/' + segment,
+          videoChunkFilename: segment
+        }
+        await server.runnerJobs.update({ jobUUID, runnerToken, jobToken: acceptedJob.jobToken, payload })
+
+        await wait(1000)
+      }
+
+      await waitJobs([ server ])
+
+      {
+        const { state } = await server.videos.get({ id: video.uuid })
+        expect(state.id).to.equal(VideoState.PUBLISHED)
+      }
+
+      await stopFfmpeg(ffmpegCommand)
+
+      await server.runnerJobs.success({ jobUUID, runnerToken, jobToken: acceptedJob.jobToken, payload: {} })
+
+      await wait(1500)
+      await waitJobs([ server ])
+
+      {
+        const { state } = await server.videos.get({ id: video.uuid })
+        expect(state.id).to.equal(VideoState.LIVE_ENDED)
+
+        const session = await server.live.findLatestSession({ videoId: video.uuid })
+        expect(session.error).to.be.null
+      }
+    })
+  })
+
+  describe('With transcoding enabled on cancelled/aborted/errored live', function () {
+    let live: LiveVideo
+    let video: Video
+    let ffmpegCommand: FfmpegCommand
+
+    async function prepare () {
+      ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey })
+      await server.runnerJobs.requestLiveJob(runnerToken)
+
+      const { job } = await server.runnerJobs.autoAccept({ runnerToken, type: 'live-rtmp-hls-transcoding' })
+
+      return job
+    }
+
+    async function checkSessionError (error: LiveVideoError) {
+      await wait(1500)
+      await waitJobs([ server ])
+
+      const session = await server.live.findLatestSession({ videoId: video.uuid })
+      expect(session.error).to.equal(error)
+    }
+
+    before(async function () {
+      await server.config.enableLive({
+        allowReplay: true,
+        resolutions: 'max',
+        transcoding: true
+      })
+
+      const data = await server.live.quickCreate({ permanentLive: true, saveReplay: false, privacy: VideoPrivacy.PUBLIC })
+      live = data.live
+      video = data.video
+    })
+
+    it('Should abort a running live', async function () {
+      this.timeout(120000)
+
+      const job = await prepare()
+
+      await Promise.all([
+        server.runnerJobs.abort({ jobUUID: job.uuid, runnerToken, jobToken: job.jobToken, reason: 'abort' }),
+        testFfmpegStreamError(ffmpegCommand, true)
+      ])
+
+      // Abort is not supported
+      await checkSessionError(LiveVideoError.RUNNER_JOB_ERROR)
+    })
+
+    it('Should cancel a running live', async function () {
+      this.timeout(120000)
+
+      const job = await prepare()
+
+      await Promise.all([
+        server.runnerJobs.cancelByAdmin({ jobUUID: job.uuid }),
+        testFfmpegStreamError(ffmpegCommand, true)
+      ])
+
+      await checkSessionError(LiveVideoError.RUNNER_JOB_CANCEL)
+    })
+
+    it('Should error a running live', async function () {
+      this.timeout(120000)
+
+      const job = await prepare()
+
+      await Promise.all([
+        server.runnerJobs.error({ jobUUID: job.uuid, runnerToken, jobToken: job.jobToken, message: 'error' }),
+        testFfmpegStreamError(ffmpegCommand, true)
+      ])
+
+      await checkSessionError(LiveVideoError.RUNNER_JOB_ERROR)
+    })
+  })
+
+  after(async function () {
+    await cleanupTests([ server ])
+  })
+})
diff --git a/server/tests/api/runners/runner-socket.ts b/server/tests/api/runners/runner-socket.ts
new file mode 100644 (file)
index 0000000..df640f9
--- /dev/null
@@ -0,0 +1,116 @@
+/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
+
+import { expect } from 'chai'
+import { wait } from '@shared/core-utils'
+import {
+  cleanupTests,
+  createSingleServer,
+  PeerTubeServer,
+  setAccessTokensToServers,
+  setDefaultVideoChannel,
+  waitJobs
+} from '@shared/server-commands'
+
+describe('Test runner socket', function () {
+  let server: PeerTubeServer
+  let runnerToken: string
+
+  before(async function () {
+    this.timeout(120_000)
+
+    server = await createSingleServer(1)
+
+    await setAccessTokensToServers([ server ])
+    await setDefaultVideoChannel([ server ])
+
+    await server.config.enableTranscoding(true, true)
+    await server.config.enableRemoteTranscoding()
+    runnerToken = await server.runners.autoRegisterRunner()
+  })
+
+  it('Should throw an error without runner token', function (done) {
+    const localSocket = server.socketIO.getRunnersSocket({ runnerToken: null })
+    localSocket.on('connect_error', err => {
+      expect(err.message).to.contain('No runner token provided')
+      done()
+    })
+  })
+
+  it('Should throw an error with a bad runner token', function (done) {
+    const localSocket = server.socketIO.getRunnersSocket({ runnerToken: 'ergag' })
+    localSocket.on('connect_error', err => {
+      expect(err.message).to.contain('Invalid runner token')
+      done()
+    })
+  })
+
+  it('Should not send ping if there is no available jobs', async function () {
+    let pings = 0
+    const localSocket = server.socketIO.getRunnersSocket({ runnerToken })
+    localSocket.on('available-jobs', () => pings++)
+
+    expect(pings).to.equal(0)
+  })
+
+  it('Should send a ping on available job', async function () {
+    let pings = 0
+    const localSocket = server.socketIO.getRunnersSocket({ runnerToken })
+    localSocket.on('available-jobs', () => pings++)
+
+    await server.videos.quickUpload({ name: 'video1' })
+
+    // Wait for debounce
+    await wait(1000)
+    await waitJobs([ server ])
+    expect(pings).to.equal(1)
+
+    await server.videos.quickUpload({ name: 'video2' })
+
+    // Wait for debounce
+    await wait(1000)
+    await waitJobs([ server ])
+    expect(pings).to.equal(2)
+
+    await server.runnerJobs.cancelAllJobs()
+  })
+
+  it('Should send a ping when a child is ready', async function () {
+    let pings = 0
+    const localSocket = server.socketIO.getRunnersSocket({ runnerToken })
+    localSocket.on('available-jobs', () => pings++)
+
+    await server.videos.quickUpload({ name: 'video3' })
+    // Wait for debounce
+    await wait(1000)
+    await waitJobs([ server ])
+
+    expect(pings).to.equal(1)
+
+    await server.runnerJobs.autoProcessWebVideoJob(runnerToken)
+    // Wait for debounce
+    await wait(1000)
+    await waitJobs([ server ])
+
+    expect(pings).to.equal(2)
+  })
+
+  it('Should not send a ping if the ended job does not have a child', async function () {
+    let pings = 0
+    const localSocket = server.socketIO.getRunnersSocket({ runnerToken })
+    localSocket.on('available-jobs', () => pings++)
+
+    const { availableJobs } = await server.runnerJobs.request({ runnerToken })
+    const job = availableJobs.find(j => j.type === 'vod-web-video-transcoding')
+    await server.runnerJobs.autoProcessWebVideoJob(runnerToken, job.uuid)
+
+    // Wait for debounce
+    await wait(1000)
+    await waitJobs([ server ])
+
+    expect(pings).to.equal(0)
+  })
+
+  after(async function () {
+    await cleanupTests([ server ])
+  })
+})
diff --git a/server/tests/api/runners/runner-vod-transcoding.ts b/server/tests/api/runners/runner-vod-transcoding.ts
new file mode 100644 (file)
index 0000000..92a47ac
--- /dev/null
@@ -0,0 +1,541 @@
+/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
+
+import { expect } from 'chai'
+import { readFile } from 'fs-extra'
+import { completeCheckHlsPlaylist } from '@server/tests/shared'
+import { buildAbsoluteFixturePath } from '@shared/core-utils'
+import {
+  HttpStatusCode,
+  RunnerJobSuccessPayload,
+  RunnerJobVODAudioMergeTranscodingPayload,
+  RunnerJobVODHLSTranscodingPayload,
+  RunnerJobVODPayload,
+  RunnerJobVODWebVideoTranscodingPayload,
+  VideoState,
+  VODAudioMergeTranscodingSuccess,
+  VODHLSTranscodingSuccess,
+  VODWebVideoTranscodingSuccess
+} from '@shared/models'
+import {
+  cleanupTests,
+  createMultipleServers,
+  doubleFollow,
+  makeGetRequest,
+  makeRawRequest,
+  PeerTubeServer,
+  setAccessTokensToServers,
+  setDefaultVideoChannel,
+  waitJobs
+} from '@shared/server-commands'
+
+async function processAllJobs (server: PeerTubeServer, runnerToken: string) {
+  do {
+    const { availableJobs } = await server.runnerJobs.requestVOD({ runnerToken })
+    if (availableJobs.length === 0) break
+
+    const { job } = await server.runnerJobs.accept<RunnerJobVODPayload>({ runnerToken, jobUUID: availableJobs[0].uuid })
+
+    const payload: RunnerJobSuccessPayload = {
+      videoFile: `video_short_${job.payload.output.resolution}p.mp4`,
+      resolutionPlaylistFile: `video_short_${job.payload.output.resolution}p.m3u8`
+    }
+    await server.runnerJobs.success({ runnerToken, jobUUID: job.uuid, jobToken: job.jobToken, payload })
+  } while (true)
+
+  await waitJobs([ server ])
+}
+
+describe('Test runner VOD transcoding', function () {
+  let servers: PeerTubeServer[] = []
+  let runnerToken: string
+
+  before(async function () {
+    this.timeout(120_000)
+
+    servers = await createMultipleServers(2)
+
+    await setAccessTokensToServers(servers)
+    await setDefaultVideoChannel(servers)
+
+    await doubleFollow(servers[0], servers[1])
+
+    await servers[0].config.enableRemoteTranscoding()
+    runnerToken = await servers[0].runners.autoRegisterRunner()
+  })
+
+  describe('Without transcoding', function () {
+
+    before(async function () {
+      this.timeout(60000)
+
+      await servers[0].config.disableTranscoding()
+      await servers[0].videos.quickUpload({ name: 'video' })
+
+      await waitJobs(servers)
+    })
+
+    it('Should not have available jobs', async function () {
+      const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
+      expect(availableJobs).to.have.lengthOf(0)
+    })
+  })
+
+  describe('With classic transcoding enabled', function () {
+
+    before(async function () {
+      this.timeout(60000)
+
+      await servers[0].config.enableTranscoding(true, true)
+    })
+
+    it('Should error a transcoding job', async function () {
+      this.timeout(60000)
+
+      await servers[0].runnerJobs.cancelAllJobs()
+      const { uuid } = await servers[0].videos.quickUpload({ name: 'video' })
+      await waitJobs(servers)
+
+      const { availableJobs } = await servers[0].runnerJobs.request({ runnerToken })
+      const jobUUID = availableJobs[0].uuid
+
+      const { job } = await servers[0].runnerJobs.accept({ runnerToken, jobUUID })
+      const jobToken = job.jobToken
+
+      await servers[0].runnerJobs.error({ runnerToken, jobUUID, jobToken, message: 'Error' })
+
+      const video = await servers[0].videos.get({ id: uuid })
+      expect(video.state.id).to.equal(VideoState.TRANSCODING_FAILED)
+    })
+
+    it('Should cancel a transcoding job', async function () {
+      await servers[0].runnerJobs.cancelAllJobs()
+      const { uuid } = await servers[0].videos.quickUpload({ name: 'video' })
+      await waitJobs(servers)
+
+      const { availableJobs } = await servers[0].runnerJobs.request({ runnerToken })
+      const jobUUID = availableJobs[0].uuid
+
+      await servers[0].runnerJobs.cancelByAdmin({ jobUUID })
+
+      const video = await servers[0].videos.get({ id: uuid })
+      expect(video.state.id).to.equal(VideoState.PUBLISHED)
+    })
+  })
+
+  describe('Web video transcoding only', function () {
+    let videoUUID: string
+    let jobToken: string
+    let jobUUID: string
+
+    before(async function () {
+      this.timeout(60000)
+
+      await servers[0].runnerJobs.cancelAllJobs()
+      await servers[0].config.enableTranscoding(true, false)
+
+      const { uuid } = await servers[0].videos.quickUpload({ name: 'web video', fixture: 'video_short.webm' })
+      videoUUID = uuid
+
+      await waitJobs(servers)
+    })
+
+    it('Should have jobs available for remote runners', async function () {
+      const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
+      expect(availableJobs).to.have.lengthOf(1)
+
+      jobUUID = availableJobs[0].uuid
+    })
+
+    it('Should have a valid first transcoding job', async function () {
+      const { job } = await servers[0].runnerJobs.accept<RunnerJobVODWebVideoTranscodingPayload>({ runnerToken, jobUUID })
+      jobToken = job.jobToken
+
+      expect(job.type === 'vod-web-video-transcoding')
+      expect(job.payload.input.videoFileUrl).to.exist
+      expect(job.payload.output.resolution).to.equal(720)
+      expect(job.payload.output.fps).to.equal(25)
+
+      const { body } = await servers[0].runnerJobs.getInputFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
+      const inputFile = await readFile(buildAbsoluteFixturePath('video_short.webm'))
+
+      expect(body).to.deep.equal(inputFile)
+    })
+
+    it('Should transcode the max video resolution and send it back to the server', async function () {
+      this.timeout(60000)
+
+      const payload: VODWebVideoTranscodingSuccess = {
+        videoFile: 'video_short.mp4'
+      }
+      await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
+
+      await waitJobs(servers)
+    })
+
+    it('Should have the video updated', async function () {
+      for (const server of servers) {
+        const video = await server.videos.get({ id: videoUUID })
+        expect(video.files).to.have.lengthOf(1)
+        expect(video.streamingPlaylists).to.have.lengthOf(0)
+
+        const { body } = await makeRawRequest({ url: video.files[0].fileUrl, expectedStatus: HttpStatusCode.OK_200 })
+        expect(body).to.deep.equal(await readFile(buildAbsoluteFixturePath('video_short.mp4')))
+      }
+    })
+
+    it('Should have 4 lower resolution to transcode', async function () {
+      const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
+      expect(availableJobs).to.have.lengthOf(4)
+
+      for (const resolution of [ 480, 360, 240, 144 ]) {
+        const job = availableJobs.find(j => j.payload.output.resolution === resolution)
+        expect(job).to.exist
+        expect(job.type).to.equal('vod-web-video-transcoding')
+
+        if (resolution === 240) jobUUID = job.uuid
+      }
+    })
+
+    it('Should process one of these transcoding jobs', async function () {
+      const { job } = await servers[0].runnerJobs.accept<RunnerJobVODWebVideoTranscodingPayload>({ runnerToken, jobUUID })
+      jobToken = job.jobToken
+
+      const { body } = await servers[0].runnerJobs.getInputFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
+      const inputFile = await readFile(buildAbsoluteFixturePath('video_short.mp4'))
+
+      expect(body).to.deep.equal(inputFile)
+
+      const payload: VODWebVideoTranscodingSuccess = { videoFile: 'video_short_240p.mp4' }
+      await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
+    })
+
+    it('Should process all other jobs', async function () {
+      const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
+      expect(availableJobs).to.have.lengthOf(3)
+
+      for (const resolution of [ 480, 360, 144 ]) {
+        const availableJob = availableJobs.find(j => j.payload.output.resolution === resolution)
+        expect(availableJob).to.exist
+        jobUUID = availableJob.uuid
+
+        const { job } = await servers[0].runnerJobs.accept<RunnerJobVODWebVideoTranscodingPayload>({ runnerToken, jobUUID })
+        jobToken = job.jobToken
+
+        const { body } = await servers[0].runnerJobs.getInputFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
+        const inputFile = await readFile(buildAbsoluteFixturePath('video_short.mp4'))
+        expect(body).to.deep.equal(inputFile)
+
+        const payload: VODWebVideoTranscodingSuccess = { videoFile: `video_short_${resolution}p.mp4` }
+        await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
+      }
+    })
+
+    it('Should have the video updated', async function () {
+      for (const server of servers) {
+        const video = await server.videos.get({ id: videoUUID })
+        expect(video.files).to.have.lengthOf(5)
+        expect(video.streamingPlaylists).to.have.lengthOf(0)
+
+        const { body } = await makeRawRequest({ url: video.files[0].fileUrl, expectedStatus: HttpStatusCode.OK_200 })
+        expect(body).to.deep.equal(await readFile(buildAbsoluteFixturePath('video_short.mp4')))
+
+        for (const file of video.files) {
+          await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 })
+          await makeRawRequest({ url: file.torrentUrl, expectedStatus: HttpStatusCode.OK_200 })
+        }
+      }
+    })
+
+    it('Should not have available jobs anymore', async function () {
+      const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
+      expect(availableJobs).to.have.lengthOf(0)
+    })
+  })
+
+  describe('HLS transcoding only', function () {
+    let videoUUID: string
+    let jobToken: string
+    let jobUUID: string
+
+    before(async function () {
+      this.timeout(60000)
+
+      await servers[0].config.enableTranscoding(false, true)
+
+      const { uuid } = await servers[0].videos.quickUpload({ name: 'hls video', fixture: 'video_short.webm' })
+      videoUUID = uuid
+
+      await waitJobs(servers)
+    })
+
+    it('Should run the optimize job', async function () {
+      this.timeout(60000)
+
+      await servers[0].runnerJobs.autoProcessWebVideoJob(runnerToken)
+    })
+
+    it('Should have 5 HLS resolution to transcode', async function () {
+      const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
+      expect(availableJobs).to.have.lengthOf(5)
+
+      for (const resolution of [ 720, 480, 360, 240, 144 ]) {
+        const job = availableJobs.find(j => j.payload.output.resolution === resolution)
+        expect(job).to.exist
+        expect(job.type).to.equal('vod-hls-transcoding')
+
+        if (resolution === 480) jobUUID = job.uuid
+      }
+    })
+
+    it('Should process one of these transcoding jobs', async function () {
+      this.timeout(60000)
+
+      const { job } = await servers[0].runnerJobs.accept<RunnerJobVODHLSTranscodingPayload>({ runnerToken, jobUUID })
+      jobToken = job.jobToken
+
+      const { body } = await servers[0].runnerJobs.getInputFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
+      const inputFile = await readFile(buildAbsoluteFixturePath('video_short.mp4'))
+
+      expect(body).to.deep.equal(inputFile)
+
+      const payload: VODHLSTranscodingSuccess = {
+        videoFile: 'video_short_480p.mp4',
+        resolutionPlaylistFile: 'video_short_480p.m3u8'
+      }
+      await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
+
+      await waitJobs(servers)
+    })
+
+    it('Should have the video updated', async function () {
+      for (const server of servers) {
+        const video = await server.videos.get({ id: videoUUID })
+
+        expect(video.files).to.have.lengthOf(1)
+        expect(video.streamingPlaylists).to.have.lengthOf(1)
+
+        const hls = video.streamingPlaylists[0]
+        expect(hls.files).to.have.lengthOf(1)
+
+        await completeCheckHlsPlaylist({ videoUUID, hlsOnly: false, servers, resolutions: [ 480 ] })
+      }
+    })
+
+    it('Should process all other jobs', async function () {
+      this.timeout(60000)
+
+      const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
+      expect(availableJobs).to.have.lengthOf(4)
+
+      let maxQualityFile = 'video_short.mp4'
+
+      for (const resolution of [ 720, 360, 240, 144 ]) {
+        const availableJob = availableJobs.find(j => j.payload.output.resolution === resolution)
+        expect(availableJob).to.exist
+        jobUUID = availableJob.uuid
+
+        const { job } = await servers[0].runnerJobs.accept<RunnerJobVODHLSTranscodingPayload>({ runnerToken, jobUUID })
+        jobToken = job.jobToken
+
+        const { body } = await servers[0].runnerJobs.getInputFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
+        const inputFile = await readFile(buildAbsoluteFixturePath(maxQualityFile))
+        expect(body).to.deep.equal(inputFile)
+
+        const payload: VODHLSTranscodingSuccess = {
+          videoFile: `video_short_${resolution}p.mp4`,
+          resolutionPlaylistFile: `video_short_${resolution}p.m3u8`
+        }
+        await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
+
+        if (resolution === 720) {
+          maxQualityFile = 'video_short_720p.mp4'
+        }
+      }
+
+      await waitJobs(servers)
+    })
+
+    it('Should have the video updated', async function () {
+      for (const server of servers) {
+        const video = await server.videos.get({ id: videoUUID })
+
+        expect(video.files).to.have.lengthOf(0)
+        expect(video.streamingPlaylists).to.have.lengthOf(1)
+
+        const hls = video.streamingPlaylists[0]
+        expect(hls.files).to.have.lengthOf(5)
+
+        await completeCheckHlsPlaylist({ videoUUID, hlsOnly: true, servers, resolutions: [ 720, 480, 360, 240, 144 ] })
+      }
+    })
+
+    it('Should not have available jobs anymore', async function () {
+      const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
+      expect(availableJobs).to.have.lengthOf(0)
+    })
+  })
+
+  describe('Web video and HLS transcoding', function () {
+
+    before(async function () {
+      this.timeout(60000)
+
+      await servers[0].config.enableTranscoding(true, true)
+
+      await servers[0].videos.quickUpload({ name: 'web video and hls video', fixture: 'video_short.webm' })
+
+      await waitJobs(servers)
+    })
+
+    it('Should process the first optimize job', async function () {
+      this.timeout(60000)
+
+      await servers[0].runnerJobs.autoProcessWebVideoJob(runnerToken)
+    })
+
+    it('Should have 9 jobs to process', async function () {
+      const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
+
+      expect(availableJobs).to.have.lengthOf(9)
+
+      const webVideoJobs = availableJobs.filter(j => j.type === 'vod-web-video-transcoding')
+      const hlsJobs = availableJobs.filter(j => j.type === 'vod-hls-transcoding')
+
+      expect(webVideoJobs).to.have.lengthOf(4)
+      expect(hlsJobs).to.have.lengthOf(5)
+    })
+
+    it('Should process all available jobs', async function () {
+      await processAllJobs(servers[0], runnerToken)
+    })
+  })
+
+  describe('Audio merge transcoding', function () {
+    let videoUUID: string
+    let jobToken: string
+    let jobUUID: string
+
+    before(async function () {
+      this.timeout(60000)
+
+      await servers[0].config.enableTranscoding(true, true)
+
+      const attributes = { name: 'audio_with_preview', previewfile: 'preview.jpg', fixture: 'sample.ogg' }
+      const { uuid } = await servers[0].videos.upload({ attributes, mode: 'legacy' })
+      videoUUID = uuid
+
+      await waitJobs(servers)
+    })
+
+    it('Should have an audio merge transcoding job', async function () {
+      const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
+      expect(availableJobs).to.have.lengthOf(1)
+
+      expect(availableJobs[0].type).to.equal('vod-audio-merge-transcoding')
+
+      jobUUID = availableJobs[0].uuid
+    })
+
+    it('Should have a valid remote audio merge transcoding job', async function () {
+      const { job } = await servers[0].runnerJobs.accept<RunnerJobVODAudioMergeTranscodingPayload>({ runnerToken, jobUUID })
+      jobToken = job.jobToken
+
+      expect(job.type === 'vod-audio-merge-transcoding')
+      expect(job.payload.input.audioFileUrl).to.exist
+      expect(job.payload.input.previewFileUrl).to.exist
+      expect(job.payload.output.resolution).to.equal(480)
+
+      {
+        const { body } = await servers[0].runnerJobs.getInputFile({ url: job.payload.input.audioFileUrl, jobToken, runnerToken })
+        const inputFile = await readFile(buildAbsoluteFixturePath('sample.ogg'))
+        expect(body).to.deep.equal(inputFile)
+      }
+
+      {
+        const { body } = await servers[0].runnerJobs.getInputFile({ url: job.payload.input.previewFileUrl, jobToken, runnerToken })
+
+        const video = await servers[0].videos.get({ id: videoUUID })
+        const { body: inputFile } = await makeGetRequest({
+          url: servers[0].url,
+          path: video.previewPath,
+          expectedStatus: HttpStatusCode.OK_200
+        })
+
+        expect(body).to.deep.equal(inputFile)
+      }
+    })
+
+    it('Should merge the audio', async function () {
+      this.timeout(60000)
+
+      const payload: VODAudioMergeTranscodingSuccess = { videoFile: 'video_short_480p.mp4' }
+      await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
+
+      await waitJobs(servers)
+    })
+
+    it('Should have the video updated', async function () {
+      for (const server of servers) {
+        const video = await server.videos.get({ id: videoUUID })
+        expect(video.files).to.have.lengthOf(1)
+        expect(video.streamingPlaylists).to.have.lengthOf(0)
+
+        const { body } = await makeRawRequest({ url: video.files[0].fileUrl, expectedStatus: HttpStatusCode.OK_200 })
+        expect(body).to.deep.equal(await readFile(buildAbsoluteFixturePath('video_short_480p.mp4')))
+      }
+    })
+
+    it('Should have 7 lower resolutions to transcode', async function () {
+      const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
+      expect(availableJobs).to.have.lengthOf(7)
+
+      for (const resolution of [ 360, 240, 144 ]) {
+        const jobs = availableJobs.filter(j => j.payload.output.resolution === resolution)
+        expect(jobs).to.have.lengthOf(2)
+      }
+
+      jobUUID = availableJobs.find(j => j.payload.output.resolution === 480).uuid
+    })
+
+    it('Should process one other job', async function () {
+      this.timeout(60000)
+
+      const { job } = await servers[0].runnerJobs.accept<RunnerJobVODHLSTranscodingPayload>({ runnerToken, jobUUID })
+      jobToken = job.jobToken
+
+      const { body } = await servers[0].runnerJobs.getInputFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
+      const inputFile = await readFile(buildAbsoluteFixturePath('video_short_480p.mp4'))
+      expect(body).to.deep.equal(inputFile)
+
+      const payload: VODHLSTranscodingSuccess = {
+        videoFile: `video_short_480p.mp4`,
+        resolutionPlaylistFile: `video_short_480p.m3u8`
+      }
+      await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
+
+      await waitJobs(servers)
+    })
+
+    it('Should have the video updated', async function () {
+      for (const server of servers) {
+        const video = await server.videos.get({ id: videoUUID })
+
+        expect(video.files).to.have.lengthOf(1)
+        expect(video.streamingPlaylists).to.have.lengthOf(1)
+
+        const hls = video.streamingPlaylists[0]
+        expect(hls.files).to.have.lengthOf(1)
+
+        await completeCheckHlsPlaylist({ videoUUID, hlsOnly: false, servers, resolutions: [ 480 ] })
+      }
+    })
+
+    it('Should process all available jobs', async function () {
+      await processAllJobs(servers[0], runnerToken)
+    })
+  })
+
+  after(async function () {
+    await cleanupTests(servers)
+  })
+})
index 3683c4ae12b5d2ac192a19fe2b6ea5fe041f6b50..54a40b994c7d65baa9145be3ca6f6bdeed1ab602 100644 (file)
@@ -63,6 +63,7 @@ function checkInitialConfig (server: PeerTubeServer, data: CustomConfig) {
   expect(data.videoChannels.maxPerUser).to.equal(20)
 
   expect(data.transcoding.enabled).to.be.false
+  expect(data.transcoding.remoteRunners.enabled).to.be.false
   expect(data.transcoding.allowAdditionalExtensions).to.be.false
   expect(data.transcoding.allowAudioFiles).to.be.false
   expect(data.transcoding.threads).to.equal(2)
@@ -87,6 +88,7 @@ function checkInitialConfig (server: PeerTubeServer, data: CustomConfig) {
   expect(data.live.maxInstanceLives).to.equal(20)
   expect(data.live.maxUserLives).to.equal(3)
   expect(data.live.transcoding.enabled).to.be.false
+  expect(data.live.transcoding.remoteRunners.enabled).to.be.false
   expect(data.live.transcoding.threads).to.equal(2)
   expect(data.live.transcoding.profile).to.equal('default')
   expect(data.live.transcoding.resolutions['144p']).to.be.false
@@ -172,6 +174,7 @@ function checkUpdatedConfig (data: CustomConfig) {
   expect(data.videoChannels.maxPerUser).to.equal(24)
 
   expect(data.transcoding.enabled).to.be.true
+  expect(data.transcoding.remoteRunners.enabled).to.be.true
   expect(data.transcoding.threads).to.equal(1)
   expect(data.transcoding.concurrency).to.equal(3)
   expect(data.transcoding.allowAdditionalExtensions).to.be.true
@@ -195,6 +198,7 @@ function checkUpdatedConfig (data: CustomConfig) {
   expect(data.live.maxInstanceLives).to.equal(-1)
   expect(data.live.maxUserLives).to.equal(10)
   expect(data.live.transcoding.enabled).to.be.true
+  expect(data.live.transcoding.remoteRunners.enabled).to.be.true
   expect(data.live.transcoding.threads).to.equal(4)
   expect(data.live.transcoding.profile).to.equal('live_profile')
   expect(data.live.transcoding.resolutions['144p']).to.be.true
@@ -313,6 +317,9 @@ const newCustomConfig: CustomConfig = {
   },
   transcoding: {
     enabled: true,
+    remoteRunners: {
+      enabled: true
+    },
     allowAdditionalExtensions: true,
     allowAudioFiles: true,
     threads: 1,
@@ -348,6 +355,9 @@ const newCustomConfig: CustomConfig = {
     maxUserLives: 10,
     transcoding: {
       enabled: true,
+      remoteRunners: {
+        enabled: true
+      },
       threads: 4,
       profile: 'live_profile',
       resolutions: {
index 704d6fc9615c239ae632b11c6d9bd9d0eaa5792c..ff53328584d145c36185e025147ac170ebbf630e 100644 (file)
@@ -146,7 +146,7 @@ describe('Test follow constraints', function () {
         const body = await servers[0].videos.get({ id: video2UUID, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
         const error = body as unknown as PeerTubeProblemDocument
 
-        const doc = 'https://docs.joinpeertube.org/api/rest-reference.html#section/Errors/does_not_respect_follow_constraints'
+        const doc = 'https://docs.joinpeertube.org/api-rest-reference.html#section/Errors/does_not_respect_follow_constraints'
         expect(error.type).to.equal(doc)
         expect(error.code).to.equal(ServerErrorCode.DOES_NOT_RESPECT_FOLLOW_CONSTRAINTS)
 
index 6a2cc2c43439d580824323514383f75de91745b3..ecec95bf85925199189ff83f36da2b7ac61b5713 100644 (file)
@@ -2,7 +2,7 @@
 
 import { expect } from 'chai'
 import { completeVideoCheck, dateIsValid, expectAccountFollows, expectChannelsFollows, testCaptionFile } from '@server/tests/shared'
-import { VideoCreateResult, VideoPrivacy } from '@shared/models'
+import { Video, VideoPrivacy } from '@shared/models'
 import { cleanupTests, createMultipleServers, PeerTubeServer, setAccessTokensToServers, waitJobs } from '@shared/server-commands'
 
 describe('Test follows', function () {
@@ -357,7 +357,7 @@ describe('Test follows', function () {
   })
 
   describe('Should propagate data on a new server follow', function () {
-    let video4: VideoCreateResult
+    let video4: Video
 
     before(async function () {
       this.timeout(50000)
@@ -372,19 +372,19 @@ describe('Test follows', function () {
 
       await servers[2].videos.upload({ attributes: { name: 'server3-2' } })
       await servers[2].videos.upload({ attributes: { name: 'server3-3' } })
-      video4 = await servers[2].videos.upload({ attributes: video4Attributes })
+      const video4CreateResult = await servers[2].videos.upload({ attributes: video4Attributes })
       await servers[2].videos.upload({ attributes: { name: 'server3-5' } })
       await servers[2].videos.upload({ attributes: { name: 'server3-6' } })
 
       {
         const userAccessToken = await servers[2].users.generateUserAndToken('captain')
 
-        await servers[2].videos.rate({ id: video4.id, rating: 'like' })
-        await servers[2].videos.rate({ token: userAccessToken, id: video4.id, rating: 'dislike' })
+        await servers[2].videos.rate({ id: video4CreateResult.id, rating: 'like' })
+        await servers[2].videos.rate({ token: userAccessToken, id: video4CreateResult.id, rating: 'dislike' })
       }
 
       {
-        await servers[2].comments.createThread({ videoId: video4.id, text: 'my super first comment' })
+        await servers[2].comments.createThread({ videoId: video4CreateResult.id, text: 'my super first comment' })
 
         await servers[2].comments.addReplyToLastThread({ text: 'my super answer to thread 1' })
         await servers[2].comments.addReplyToLastReply({ text: 'my super answer to answer of thread 1' })
@@ -392,20 +392,20 @@ describe('Test follows', function () {
       }
 
       {
-        const { id: threadId } = await servers[2].comments.createThread({ videoId: video4.id, text: 'will be deleted' })
+        const { id: threadId } = await servers[2].comments.createThread({ videoId: video4CreateResult.id, text: 'will be deleted' })
         await servers[2].comments.addReplyToLastThread({ text: 'answer to deleted' })
 
         const { id: replyId } = await servers[2].comments.addReplyToLastThread({ text: 'will also be deleted' })
 
         await servers[2].comments.addReplyToLastReply({ text: 'my second answer to deleted' })
 
-        await servers[2].comments.delete({ videoId: video4.id, commentId: threadId })
-        await servers[2].comments.delete({ videoId: video4.id, commentId: replyId })
+        await servers[2].comments.delete({ videoId: video4CreateResult.id, commentId: threadId })
+        await servers[2].comments.delete({ videoId: video4CreateResult.id, commentId: replyId })
       }
 
       await servers[2].captions.add({
         language: 'ar',
-        videoId: video4.id,
+        videoId: video4CreateResult.id,
         fixture: 'subtitle-good2.vtt'
       })
 
@@ -479,7 +479,12 @@ describe('Test follows', function () {
           }
         ]
       }
-      await completeVideoCheck(servers[0], video4, checkAttributes)
+      await completeVideoCheck({
+        server: servers[0],
+        originServer: servers[2],
+        videoUUID: video4.uuid,
+        attributes: checkAttributes
+      })
     })
 
     it('Should have propagated comments', async function () {
index 1fb4d18f9d2f1f0943471e8eb10c6436383ad9b6..0bbd9ef474600f02ca4f5ca95fd6f14107dcc082 100644 (file)
@@ -1,7 +1,7 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
 import { expect } from 'chai'
-import { completeVideoCheck } from '@server/tests/shared'
+import { completeVideoCheck, SQLCommand } from '@server/tests/shared'
 import { wait } from '@shared/core-utils'
 import { HttpStatusCode, JobState, VideoCreateResult, VideoPrivacy } from '@shared/models'
 import {
@@ -16,6 +16,8 @@ import {
 
 describe('Test handle downs', function () {
   let servers: PeerTubeServer[] = []
+  let sqlCommands: SQLCommand[]
+
   let threadIdServer1: number
   let threadIdServer2: number
   let commentIdServer1: number
@@ -88,6 +90,8 @@ describe('Test handle downs', function () {
 
     // Get the access tokens
     await setAccessTokensToServers(servers)
+
+    sqlCommands = servers.map(s => new SQLCommand(s))
   })
 
   it('Should remove followers that are often down', async function () {
@@ -209,7 +213,7 @@ describe('Test handle downs', function () {
 
     // Check unlisted video
     const video = await servers[2].videos.get({ id: unlistedVideo.uuid })
-    await completeVideoCheck(servers[2], video, unlistedCheckAttributes)
+    await completeVideoCheck({ server: servers[2], originServer: servers[0], videoUUID: video.uuid, attributes: unlistedCheckAttributes })
   })
 
   it('Should send comments on a video to server 3, and automatically fetch the video', async function () {
@@ -292,7 +296,7 @@ describe('Test handle downs', function () {
     }
 
     await waitJobs(servers)
-    await servers[1].sql.setActorFollowScores(20)
+    await sqlCommands[1].setActorFollowScores(20)
 
     // Wait video expiration
     await wait(11000)
@@ -325,6 +329,10 @@ describe('Test handle downs', function () {
   })
 
   after(async function () {
+    for (const sqlCommand of sqlCommands) {
+      await sqlCommand.cleanup()
+    }
+
     await cleanupTests(servers)
   })
 })
index 8ac7023ebc47f4fab5cbe7cab55d46384023ceb9..199d205c79e86cf0ac3fb2197cd2866b44239a0e 100644 (file)
@@ -3,7 +3,7 @@
 import { expect } from 'chai'
 import { pathExists, remove } from 'fs-extra'
 import { join } from 'path'
-import { testHelloWorldRegisteredSettings } from '@server/tests/shared'
+import { SQLCommand, testHelloWorldRegisteredSettings } from '@server/tests/shared'
 import { wait } from '@shared/core-utils'
 import { HttpStatusCode, PluginType } from '@shared/models'
 import {
@@ -17,7 +17,8 @@ import {
 } from '@shared/server-commands'
 
 describe('Test plugins', function () {
-  let server: PeerTubeServer = null
+  let server: PeerTubeServer
+  let sqlCommand: SQLCommand
   let command: PluginsCommand
 
   before(async function () {
@@ -32,6 +33,8 @@ describe('Test plugins', function () {
     await setAccessTokensToServers([ server ])
 
     command = server.plugins
+
+    sqlCommand = new SQLCommand(server)
   })
 
   it('Should list and search available plugins and themes', async function () {
@@ -236,7 +239,7 @@ describe('Test plugins', function () {
 
     async function testUpdate (type: 'plugin' | 'theme', name: string) {
       // Fake update our plugin version
-      await server.sql.setPluginVersion(name, '0.0.1')
+      await sqlCommand.setPluginVersion(name, '0.0.1')
 
       // Fake update package.json
       const packageJSON = await command.getPackageJSON(`peertube-${type}-${name}`)
@@ -366,7 +369,7 @@ describe('Test plugins', function () {
     })
 
     const query = `UPDATE "application" SET "nodeABIVersion" = 1`
-    await server.sql.updateQuery(query)
+    await sqlCommand.updateQuery(query)
 
     const baseNativeModule = server.servers.buildDirectory(join('plugins', 'node_modules', 'a-native-example'))
 
@@ -401,6 +404,8 @@ describe('Test plugins', function () {
   })
 
   after(async function () {
+    await sqlCommand.cleanup()
+
     await cleanupTests([ server ])
   })
 })
index b72f5fdbeed7e07edbcfa183c038f4d74225a2a3..1e31418e78b5b2f663d55b6f453edebe661da4c7 100644 (file)
@@ -1,7 +1,7 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
 import { expect } from 'chai'
-import { getAudioStream, getVideoStreamDimensionsInfo } from '@server/helpers/ffmpeg'
+import { getAudioStream, getVideoStreamDimensionsInfo } from '@shared/ffmpeg'
 import {
   cleanupTests,
   createMultipleServers,
index c591f5f6f8ee464a08dfb5b36b0a35b75c8d0e54..fa78b58bb438c7fa9414f28f5c1943550d240405 100644 (file)
@@ -1,18 +1,18 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
 import { expect } from 'chai'
-import { canDoQuickTranscode } from '@server/helpers/ffmpeg'
-import { generateHighBitrateVideo, generateVideoWithFramerate } from '@server/tests/shared'
+import { canDoQuickTranscode } from '@server/lib/transcoding/transcoding-quick-transcode'
+import { checkWebTorrentWorks, generateHighBitrateVideo, generateVideoWithFramerate } from '@server/tests/shared'
 import { buildAbsoluteFixturePath, getAllFiles, getMaxBitrate, getMinLimitBitrate, omit } from '@shared/core-utils'
 import {
-  buildFileMetadata,
+  ffprobePromise,
   getAudioStream,
   getVideoStreamBitrate,
   getVideoStreamDimensionsInfo,
   getVideoStreamFPS,
   hasAudioStream
-} from '@shared/extra-utils'
-import { HttpStatusCode, VideoState } from '@shared/models'
+} from '@shared/ffmpeg'
+import { HttpStatusCode, VideoFileMetadata, VideoState } from '@shared/models'
 import {
   cleanupTests,
   createMultipleServers,
@@ -20,8 +20,7 @@ import {
   makeGetRequest,
   PeerTubeServer,
   setAccessTokensToServers,
-  waitJobs,
-  webtorrentAdd
+  waitJobs
 } from '@shared/server-commands'
 
 function updateConfigForTranscoding (server: PeerTubeServer) {
@@ -90,10 +89,7 @@ describe('Test video transcoding', function () {
         const magnetUri = videoDetails.files[0].magnetUri
         expect(magnetUri).to.match(/\.webm/)
 
-        const torrent = await webtorrentAdd(magnetUri, true)
-        expect(torrent.files).to.be.an('array')
-        expect(torrent.files.length).to.equal(1)
-        expect(torrent.files[0].path).match(/\.webm$/)
+        await checkWebTorrentWorks(magnetUri, /\.webm$/)
       }
     })
 
@@ -120,10 +116,7 @@ describe('Test video transcoding', function () {
         const magnetUri = videoDetails.files[0].magnetUri
         expect(magnetUri).to.match(/\.mp4/)
 
-        const torrent = await webtorrentAdd(magnetUri, true)
-        expect(torrent.files).to.be.an('array')
-        expect(torrent.files.length).to.equal(1)
-        expect(torrent.files[0].path).match(/\.mp4$/)
+        await checkWebTorrentWorks(magnetUri, /\.mp4$/)
       }
     })
 
@@ -639,7 +632,9 @@ describe('Test video transcoding', function () {
         const video = await servers[1].videos.get({ id: videoUUID })
         const file = video.files.find(f => f.resolution.id === 240)
         const path = servers[1].servers.buildWebTorrentFilePath(file.fileUrl)
-        const metadata = await buildFileMetadata(path)
+
+        const probe = await ffprobePromise(path)
+        const metadata = new VideoFileMetadata(probe)
 
         // expected format properties
         for (const p of [
index 6a3da5ea21297f27593b062576466838a4d16192..15361587590a02e81388e062d4d0a399f13e55af 100644 (file)
@@ -1,12 +1,14 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
 import { expect } from 'chai'
+import { SQLCommand } from '@server/tests/shared'
 import { wait } from '@shared/core-utils'
 import { HttpStatusCode, OAuth2ErrorCode, PeerTubeProblemDocument } from '@shared/models'
 import { cleanupTests, createSingleServer, killallServers, PeerTubeServer, setAccessTokensToServers } from '@shared/server-commands'
 
 describe('Test oauth', function () {
   let server: PeerTubeServer
+  let sqlCommand: SQLCommand
 
   before(async function () {
     this.timeout(30000)
@@ -20,6 +22,8 @@ describe('Test oauth', function () {
     })
 
     await setAccessTokensToServers([ server ])
+
+    sqlCommand = new SQLCommand(server)
   })
 
   describe('OAuth client', function () {
@@ -118,8 +122,8 @@ describe('Test oauth', function () {
     it('Should have an expired access token', async function () {
       this.timeout(60000)
 
-      await server.sql.setTokenField(server.accessToken, 'accessTokenExpiresAt', new Date().toISOString())
-      await server.sql.setTokenField(server.accessToken, 'refreshTokenExpiresAt', new Date().toISOString())
+      await sqlCommand.setTokenField(server.accessToken, 'accessTokenExpiresAt', new Date().toISOString())
+      await sqlCommand.setTokenField(server.accessToken, 'refreshTokenExpiresAt', new Date().toISOString())
 
       await killallServers([ server ])
       await server.run()
@@ -135,7 +139,7 @@ describe('Test oauth', function () {
       this.timeout(50000)
 
       const futureDate = new Date(new Date().getTime() + 1000 * 60).toISOString()
-      await server.sql.setTokenField(server.accessToken, 'refreshTokenExpiresAt', futureDate)
+      await sqlCommand.setTokenField(server.accessToken, 'refreshTokenExpiresAt', futureDate)
 
       await killallServers([ server ])
       await server.run()
@@ -187,6 +191,7 @@ describe('Test oauth', function () {
   })
 
   after(async function () {
+    await sqlCommand.cleanup()
     await cleanupTests([ server ])
   })
 })
index ff730287a20e3abf02436896b879abbc3819d16c..a52a04e07dd1b7b1b7944c447a477df09cbe4693 100644 (file)
@@ -5,6 +5,7 @@ import request from 'supertest'
 import {
   checkTmpIsEmpty,
   checkVideoFilesWereRemoved,
+  checkWebTorrentWorks,
   completeVideoCheck,
   dateIsValid,
   saveVideoInServers,
@@ -21,8 +22,7 @@ import {
   setAccessTokensToServers,
   setDefaultAccountAvatar,
   setDefaultChannelAvatar,
-  waitJobs,
-  webtorrentAdd
+  waitJobs
 } from '@shared/server-commands'
 
 describe('Test multiple servers', function () {
@@ -134,7 +134,7 @@ describe('Test multiple servers', function () {
         expect(data.length).to.equal(1)
         const video = data[0]
 
-        await completeVideoCheck(server, video, checkAttributes)
+        await completeVideoCheck({ server, originServer: servers[0], videoUUID: video.uuid, attributes: checkAttributes })
         publishedAt = video.publishedAt as string
 
         expect(video.channel.avatars).to.have.lengthOf(2)
@@ -238,7 +238,7 @@ describe('Test multiple servers', function () {
         expect(data.length).to.equal(2)
         const video = data[1]
 
-        await completeVideoCheck(server, video, checkAttributes)
+        await completeVideoCheck({ server, originServer: servers[1], videoUUID: video.uuid, attributes: checkAttributes })
       }
     })
 
@@ -328,7 +328,7 @@ describe('Test multiple servers', function () {
             }
           ]
         }
-        await completeVideoCheck(server, video1, checkAttributesVideo1)
+        await completeVideoCheck({ server, originServer: servers[2], videoUUID: video1.uuid, attributes: checkAttributesVideo1 })
 
         const checkAttributesVideo2 = {
           name: 'my super name for server 3-2',
@@ -362,7 +362,7 @@ describe('Test multiple servers', function () {
             }
           ]
         }
-        await completeVideoCheck(server, video2, checkAttributesVideo2)
+        await completeVideoCheck({ server, originServer: servers[2], videoUUID: video2.uuid, attributes: checkAttributesVideo2 })
       }
     })
   })
@@ -408,10 +408,8 @@ describe('Test multiple servers', function () {
       toRemove.push(data[3])
 
       const videoDetails = await servers[2].videos.get({ id: video.id })
-      const torrent = await webtorrentAdd(videoDetails.files[0].magnetUri, true)
-      expect(torrent.files).to.be.an('array')
-      expect(torrent.files.length).to.equal(1)
-      expect(torrent.files[0].path).to.exist.and.to.not.equal('')
+
+      await checkWebTorrentWorks(videoDetails.files[0].magnetUri)
     })
 
     it('Should add the file 2 by asking server 1', async function () {
@@ -422,10 +420,7 @@ describe('Test multiple servers', function () {
       const video = data[1]
       const videoDetails = await servers[0].videos.get({ id: video.id })
 
-      const torrent = await webtorrentAdd(videoDetails.files[0].magnetUri, true)
-      expect(torrent.files).to.be.an('array')
-      expect(torrent.files.length).to.equal(1)
-      expect(torrent.files[0].path).to.exist.and.to.not.equal('')
+      await checkWebTorrentWorks(videoDetails.files[0].magnetUri)
     })
 
     it('Should add the file 3 by asking server 2', async function () {
@@ -436,10 +431,7 @@ describe('Test multiple servers', function () {
       const video = data[2]
       const videoDetails = await servers[1].videos.get({ id: video.id })
 
-      const torrent = await webtorrentAdd(videoDetails.files[0].magnetUri, true)
-      expect(torrent.files).to.be.an('array')
-      expect(torrent.files.length).to.equal(1)
-      expect(torrent.files[0].path).to.exist.and.to.not.equal('')
+      await checkWebTorrentWorks(videoDetails.files[0].magnetUri)
     })
 
     it('Should add the file 3-2 by asking server 1', async function () {
@@ -450,10 +442,7 @@ describe('Test multiple servers', function () {
       const video = data[3]
       const videoDetails = await servers[0].videos.get({ id: video.id })
 
-      const torrent = await webtorrentAdd(videoDetails.files[0].magnetUri)
-      expect(torrent.files).to.be.an('array')
-      expect(torrent.files.length).to.equal(1)
-      expect(torrent.files[0].path).to.exist.and.to.not.equal('')
+      await checkWebTorrentWorks(videoDetails.files[0].magnetUri)
     })
 
     it('Should add the file 2 in 360p by asking server 1', async function () {
@@ -467,10 +456,7 @@ describe('Test multiple servers', function () {
       const file = videoDetails.files.find(f => f.resolution.id === 360)
       expect(file).not.to.be.undefined
 
-      const torrent = await webtorrentAdd(file.magnetUri)
-      expect(torrent.files).to.be.an('array')
-      expect(torrent.files.length).to.equal(1)
-      expect(torrent.files[0].path).to.exist.and.to.not.equal('')
+      await checkWebTorrentWorks(file.magnetUri)
     })
   })
 
@@ -685,7 +671,7 @@ describe('Test multiple servers', function () {
           thumbnailfile: 'thumbnail',
           previewfile: 'preview'
         }
-        await completeVideoCheck(server, videoUpdated, checkAttributes)
+        await completeVideoCheck({ server, originServer: servers[2], videoUUID: videoUpdated.uuid, attributes: checkAttributes })
       }
     })
 
@@ -1087,7 +1073,7 @@ describe('Test multiple servers', function () {
             }
           ]
         }
-        await completeVideoCheck(server, video, checkAttributes)
+        await completeVideoCheck({ server, originServer: servers[1], videoUUID: video.uuid, attributes: checkAttributes })
       }
     })
   })
index 0cf1e66757f82a484851837d0770ed1cc3442130..a70a7258b9d87f3738d89a74bcc41a08cc7a7ddc 100644 (file)
@@ -261,7 +261,7 @@ describe('Test resumable upload', function () {
         pathUploadId: uploadId,
         token: server.accessToken,
         digestBuilder: () => 'sha=' + 'a'.repeat(40),
-        expectedStatus: 460
+        expectedStatus: 460 as any
       })
     })
 
index e8e981e556cecf68d4dd9fa4da3bf3843db01369..72f833ec2da9eb25f2b3d7810bd27bbea0b45c37 100644 (file)
@@ -164,14 +164,14 @@ describe('Test a single server', function () {
       expect(data.length).to.equal(1)
 
       const video = data[0]
-      await completeVideoCheck(server, video, getCheckAttributes())
+      await completeVideoCheck({ server, originServer: server, videoUUID: video.uuid, attributes: getCheckAttributes() })
     })
 
     it('Should get the video by UUID', async function () {
       this.timeout(5000)
 
       const video = await server.videos.get({ id: videoUUID })
-      await completeVideoCheck(server, video, getCheckAttributes())
+      await completeVideoCheck({ server, originServer: server, videoUUID: video.uuid, attributes: getCheckAttributes() })
     })
 
     it('Should have the views updated', async function () {
@@ -360,7 +360,7 @@ describe('Test a single server', function () {
 
       const video = await server.videos.get({ id: videoId })
 
-      await completeVideoCheck(server, video, updateCheckAttributes())
+      await completeVideoCheck({ server, originServer: server, videoUUID: video.uuid, attributes: updateCheckAttributes() })
     })
 
     it('Should update only the tags of a video', async function () {
@@ -371,7 +371,12 @@ describe('Test a single server', function () {
 
       const video = await server.videos.get({ id: videoId })
 
-      await completeVideoCheck(server, video, Object.assign(updateCheckAttributes(), attributes))
+      await completeVideoCheck({
+        server,
+        originServer: server,
+        videoUUID: video.uuid,
+        attributes: Object.assign(updateCheckAttributes(), attributes)
+      })
     })
 
     it('Should update only the description of a video', async function () {
@@ -382,8 +387,12 @@ describe('Test a single server', function () {
 
       const video = await server.videos.get({ id: videoId })
 
-      const expectedAttributes = Object.assign(updateCheckAttributes(), { tags: [ 'supertag', 'tag1', 'tag2' ] }, attributes)
-      await completeVideoCheck(server, video, expectedAttributes)
+      await completeVideoCheck({
+        server,
+        originServer: server,
+        videoUUID: video.uuid,
+        attributes: Object.assign(updateCheckAttributes(), { tags: [ 'supertag', 'tag1', 'tag2' ] }, attributes)
+      })
     })
 
     it('Should like a video', async function () {
index dd483f95ecbdd0d654e409648e97256eef4ebad0..a31e48d1d2524e2f93e1a643dd6a69bac1ae2a72 100644 (file)
@@ -1,7 +1,7 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
 import { expect } from 'chai'
-import { FIXTURE_URLS } from '@server/tests/shared'
+import { FIXTURE_URLS, SQLCommand } from '@server/tests/shared'
 import { areHttpImportTestsDisabled } from '@shared/core-utils'
 import { VideoChannelSyncState, VideoInclude, VideoPrivacy } from '@shared/models'
 import {
@@ -23,6 +23,7 @@ describe('Test channel synchronizations', function () {
 
     describe('Sync using ' + mode, function () {
       let servers: PeerTubeServer[]
+      let sqlCommands: SQLCommand[]
 
       let startTestDate: Date
 
@@ -36,7 +37,7 @@ describe('Test channel synchronizations', function () {
       }
 
       async function changeDateForSync (channelSyncId: number, newDate: string) {
-        await servers[0].sql.updateQuery(
+        await sqlCommands[0].updateQuery(
           `UPDATE "videoChannelSync" ` +
           `SET "createdAt"='${newDate}', "lastSyncAt"='${newDate}' ` +
           `WHERE id=${channelSyncId}`
@@ -82,6 +83,8 @@ describe('Test channel synchronizations', function () {
           const { videoChannels } = await servers[0].users.getMyInfo({ token: userInfo.accessToken })
           userInfo.channelId = videoChannels[0].id
         }
+
+        sqlCommands = servers.map(s => new SQLCommand(s))
       })
 
       it('Should fetch the latest channel videos of a remote channel', async function () {
@@ -302,6 +305,10 @@ describe('Test channel synchronizations', function () {
       })
 
       after(async function () {
+        for (const sqlCommand of sqlCommands) {
+          await sqlCommand.cleanup()
+        }
+
         await killallServers(servers)
       })
     })
index 64bd4d9ae4260e01777d74c869ec2fb1d93f4768..c82ad6f16749e5ed8af60f541b2045d43a30d562 100644 (file)
@@ -3,7 +3,7 @@
 import { expect } from 'chai'
 import { basename } from 'path'
 import { ACTOR_IMAGES_SIZE } from '@server/initializers/constants'
-import { testFileExistsOrNot, testImage } from '@server/tests/shared'
+import { SQLCommand, testFileExistsOrNot, testImage } from '@server/tests/shared'
 import { wait } from '@shared/core-utils'
 import { ActorImageType, User, VideoChannel } from '@shared/models'
 import {
@@ -25,6 +25,8 @@ async function findChannel (server: PeerTubeServer, channelId: number) {
 
 describe('Test video channels', function () {
   let servers: PeerTubeServer[]
+  let sqlCommands: SQLCommand[]
+
   let userInfo: User
   let secondVideoChannelId: number
   let totoChannel: number
@@ -45,6 +47,8 @@ describe('Test video channels', function () {
     await setDefaultAccountAvatar(servers)
 
     await doubleFollow(servers[0], servers[1])
+
+    sqlCommands = servers.map(s => new SQLCommand(s))
   })
 
   it('Should have one video channel (created with root)', async () => {
@@ -278,7 +282,9 @@ describe('Test video channels', function () {
 
     await waitJobs(servers)
 
-    for (const server of servers) {
+    for (let i = 0; i < servers.length; i++) {
+      const server = servers[i]
+
       const videoChannel = await findChannel(server, secondVideoChannelId)
       const expectedSizes = ACTOR_IMAGES_SIZE[ActorImageType.AVATAR]
 
@@ -289,7 +295,7 @@ describe('Test video channels', function () {
         await testImage(server.url, `avatar-resized-${avatar.width}x${avatar.width}`, avatarPaths[server.port], '.png')
         await testFileExistsOrNot(server, 'avatars', basename(avatarPaths[server.port]), true)
 
-        const row = await server.sql.getActorImage(basename(avatarPaths[server.port]))
+        const row = await sqlCommands[i].getActorImage(basename(avatarPaths[server.port]))
 
         expect(expectedSizes.some(({ height, width }) => row.height === height && row.width === width)).to.equal(true)
       }
@@ -309,14 +315,16 @@ describe('Test video channels', function () {
 
     await waitJobs(servers)
 
-    for (const server of servers) {
+    for (let i = 0; i < servers.length; i++) {
+      const server = servers[i]
+
       const videoChannel = await server.channels.get({ channelName: 'second_video_channel@' + servers[0].host })
 
       bannerPaths[server.port] = videoChannel.banners[0].path
       await testImage(server.url, 'banner-resized', bannerPaths[server.port])
       await testFileExistsOrNot(server, 'avatars', basename(bannerPaths[server.port]), true)
 
-      const row = await server.sql.getActorImage(basename(bannerPaths[server.port]))
+      const row = await sqlCommands[i].getActorImage(basename(bannerPaths[server.port]))
       expect(row.height).to.equal(ACTOR_IMAGES_SIZE[ActorImageType.BANNER][0].height)
       expect(row.width).to.equal(ACTOR_IMAGES_SIZE[ActorImageType.BANNER][0].width)
     }
@@ -546,6 +554,10 @@ describe('Test video channels', function () {
   })
 
   after(async function () {
+    for (const sqlCommand of sqlCommands) {
+      await sqlCommand.cleanup()
+    }
+
     await cleanupTests(servers)
   })
 })
index 2dcfbbc57054d895c1eacf84369dfca538b81412..54284853342aff6ec41e93c5e0c0b85c02637d4b 100644 (file)
@@ -2,7 +2,7 @@
 
 import { expect } from 'chai'
 import { decode } from 'magnet-uri'
-import { checkVideoFileTokenReinjection, expectStartWith } from '@server/tests/shared'
+import { checkVideoFileTokenReinjection, expectStartWith, parseTorrentVideo } from '@server/tests/shared'
 import { getAllFiles, wait } from '@shared/core-utils'
 import { HttpStatusCode, LiveVideo, VideoDetails, VideoPrivacy } from '@shared/models'
 import {
@@ -10,7 +10,6 @@ import {
   createSingleServer,
   findExternalSavedVideo,
   makeRawRequest,
-  parseTorrentVideo,
   PeerTubeServer,
   sendRTMPStream,
   setAccessTokensToServers,
index 7c543a74a91f94de2f88fda6c97d3d7bba9bd10f..fce2d538cb953f2d47449f2cdd7162b1234cd442 100644 (file)
@@ -1,6 +1,7 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
 import { expect } from 'chai'
+import { SQLCommand } from '@server/tests/shared'
 import { wait } from '@shared/core-utils'
 import {
   cleanupTests,
@@ -14,6 +15,7 @@ import {
 
 describe('Test video views cleaner', function () {
   let servers: PeerTubeServer[]
+  let sqlCommands: SQLCommand[]
 
   let videoIdServer1: string
   let videoIdServer2: string
@@ -37,6 +39,8 @@ describe('Test video views cleaner', function () {
     await servers[1].views.simulateView({ id: videoIdServer2 })
 
     await waitJobs(servers)
+
+    sqlCommands = servers.map(s => new SQLCommand(s))
   })
 
   it('Should not clean old video views', async function () {
@@ -50,18 +54,14 @@ describe('Test video views cleaner', function () {
 
     // Should still have views
 
-    {
-      for (const server of servers) {
-        const total = await server.sql.countVideoViewsOf(videoIdServer1)
-        expect(total).to.equal(2, 'Server ' + server.serverNumber + ' does not have the correct amount of views')
-      }
+    for (let i = 0; i < servers.length; i++) {
+      const total = await sqlCommands[i].countVideoViewsOf(videoIdServer1)
+      expect(total).to.equal(2, 'Server ' + servers[i].serverNumber + ' does not have the correct amount of views')
     }
 
-    {
-      for (const server of servers) {
-        const total = await server.sql.countVideoViewsOf(videoIdServer2)
-        expect(total).to.equal(2, 'Server ' + server.serverNumber + ' does not have the correct amount of views')
-      }
+    for (let i = 0; i < servers.length; i++) {
+      const total = await sqlCommands[i].countVideoViewsOf(videoIdServer2)
+      expect(total).to.equal(2, 'Server ' + servers[i].serverNumber + ' does not have the correct amount of views')
     }
   })
 
@@ -76,23 +76,23 @@ describe('Test video views cleaner', function () {
 
     // Should still have views
 
-    {
-      for (const server of servers) {
-        const total = await server.sql.countVideoViewsOf(videoIdServer1)
-        expect(total).to.equal(2)
-      }
+    for (let i = 0; i < servers.length; i++) {
+      const total = await sqlCommands[i].countVideoViewsOf(videoIdServer1)
+      expect(total).to.equal(2)
     }
 
-    {
-      const totalServer1 = await servers[0].sql.countVideoViewsOf(videoIdServer2)
-      expect(totalServer1).to.equal(0)
+    const totalServer1 = await sqlCommands[0].countVideoViewsOf(videoIdServer2)
+    expect(totalServer1).to.equal(0)
 
-      const totalServer2 = await servers[1].sql.countVideoViewsOf(videoIdServer2)
-      expect(totalServer2).to.equal(2)
-    }
+    const totalServer2 = await sqlCommands[1].countVideoViewsOf(videoIdServer2)
+    expect(totalServer2).to.equal(2)
   })
 
   after(async function () {
+    for (const sqlCommand of sqlCommands) {
+      await sqlCommand.cleanup()
+    }
+
     await cleanupTests(servers)
   })
 })
diff --git a/server/tests/cli/create-transcoding-job.ts b/server/tests/cli/create-transcoding-job.ts
deleted file mode 100644 (file)
index 38b7378..0000000
+++ /dev/null
@@ -1,262 +0,0 @@
-/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
-
-import { expect } from 'chai'
-import { areMockObjectStorageTestsDisabled } from '@shared/core-utils'
-import { HttpStatusCode, VideoFile } from '@shared/models'
-import {
-  cleanupTests,
-  createMultipleServers,
-  doubleFollow,
-  makeRawRequest,
-  ObjectStorageCommand,
-  PeerTubeServer,
-  setAccessTokensToServers,
-  waitJobs
-} from '@shared/server-commands'
-import { checkResolutionsInMasterPlaylist, expectStartWith } from '../shared'
-
-async function checkFilesInObjectStorage (files: VideoFile[], type: 'webtorrent' | 'playlist') {
-  for (const file of files) {
-    const shouldStartWith = type === 'webtorrent'
-      ? ObjectStorageCommand.getMockWebTorrentBaseUrl()
-      : ObjectStorageCommand.getMockPlaylistBaseUrl()
-
-    expectStartWith(file.fileUrl, shouldStartWith)
-
-    await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 })
-  }
-}
-
-function runTests (objectStorage: boolean) {
-  let servers: PeerTubeServer[] = []
-  const videosUUID: string[] = []
-  const publishedAt: string[] = []
-
-  before(async function () {
-    this.timeout(120000)
-
-    const config = objectStorage
-      ? ObjectStorageCommand.getDefaultMockConfig()
-      : {}
-
-    // Run server 2 to have transcoding enabled
-    servers = await createMultipleServers(2, config)
-    await setAccessTokensToServers(servers)
-
-    await servers[0].config.disableTranscoding()
-
-    await doubleFollow(servers[0], servers[1])
-
-    if (objectStorage) await ObjectStorageCommand.prepareDefaultMockBuckets()
-
-    for (let i = 1; i <= 5; i++) {
-      const { uuid, shortUUID } = await servers[0].videos.upload({ attributes: { name: 'video' + i } })
-
-      await waitJobs(servers)
-
-      const video = await servers[0].videos.get({ id: uuid })
-      publishedAt.push(video.publishedAt as string)
-
-      if (i > 2) {
-        videosUUID.push(uuid)
-      } else {
-        videosUUID.push(shortUUID)
-      }
-    }
-
-    await waitJobs(servers)
-  })
-
-  it('Should have two video files on each server', async function () {
-    this.timeout(30000)
-
-    for (const server of servers) {
-      const { data } = await server.videos.list()
-      expect(data).to.have.lengthOf(videosUUID.length)
-
-      for (const video of data) {
-        const videoDetail = await server.videos.get({ id: video.uuid })
-        expect(videoDetail.files).to.have.lengthOf(1)
-        expect(videoDetail.streamingPlaylists).to.have.lengthOf(0)
-      }
-    }
-  })
-
-  it('Should run a transcoding job on video 2', async function () {
-    this.timeout(60000)
-
-    await servers[0].cli.execWithEnv(`npm run create-transcoding-job -- -v ${videosUUID[1]}`)
-    await waitJobs(servers)
-
-    for (const server of servers) {
-      const { data } = await server.videos.list()
-
-      let infoHashes: { [id: number]: string }
-
-      for (const video of data) {
-        const videoDetails = await server.videos.get({ id: video.uuid })
-
-        if (video.shortUUID === videosUUID[1] || video.uuid === videosUUID[1]) {
-          expect(videoDetails.files).to.have.lengthOf(4)
-          expect(videoDetails.streamingPlaylists).to.have.lengthOf(0)
-
-          if (objectStorage) await checkFilesInObjectStorage(videoDetails.files, 'webtorrent')
-
-          if (!infoHashes) {
-            infoHashes = {}
-
-            for (const file of videoDetails.files) {
-              infoHashes[file.resolution.id.toString()] = file.magnetUri
-            }
-          } else {
-            for (const resolution of Object.keys(infoHashes)) {
-              const file = videoDetails.files.find(f => f.resolution.id.toString() === resolution)
-              expect(file.magnetUri).to.equal(infoHashes[resolution])
-            }
-          }
-        } else {
-          expect(videoDetails.files).to.have.lengthOf(1)
-          expect(videoDetails.streamingPlaylists).to.have.lengthOf(0)
-        }
-      }
-    }
-  })
-
-  it('Should run a transcoding job on video 1 with resolution', async function () {
-    this.timeout(60000)
-
-    await servers[0].cli.execWithEnv(`npm run create-transcoding-job -- -v ${videosUUID[0]} -r 480`)
-
-    await waitJobs(servers)
-
-    for (const server of servers) {
-      const { data } = await server.videos.list()
-      expect(data).to.have.lengthOf(videosUUID.length)
-
-      const videoDetails = await server.videos.get({ id: videosUUID[0] })
-
-      expect(videoDetails.files).to.have.lengthOf(2)
-      expect(videoDetails.files[0].resolution.id).to.equal(720)
-      expect(videoDetails.files[1].resolution.id).to.equal(480)
-
-      expect(videoDetails.streamingPlaylists).to.have.lengthOf(0)
-
-      if (objectStorage) await checkFilesInObjectStorage(videoDetails.files, 'webtorrent')
-    }
-  })
-
-  it('Should generate an HLS resolution', async function () {
-    this.timeout(120000)
-
-    await servers[0].cli.execWithEnv(`npm run create-transcoding-job -- -v ${videosUUID[2]} --generate-hls -r 480`)
-
-    await waitJobs(servers)
-
-    for (const server of servers) {
-      const videoDetails = await server.videos.get({ id: videosUUID[2] })
-
-      expect(videoDetails.files).to.have.lengthOf(1)
-      if (objectStorage) await checkFilesInObjectStorage(videoDetails.files, 'webtorrent')
-
-      expect(videoDetails.streamingPlaylists).to.have.lengthOf(1)
-
-      const hlsPlaylist = videoDetails.streamingPlaylists[0]
-
-      const files = hlsPlaylist.files
-      expect(files).to.have.lengthOf(1)
-      expect(files[0].resolution.id).to.equal(480)
-
-      if (objectStorage) {
-        await checkFilesInObjectStorage(files, 'playlist')
-
-        const resolutions = files.map(f => f.resolution.id)
-        await checkResolutionsInMasterPlaylist({ server, playlistUrl: hlsPlaylist.playlistUrl, resolutions })
-      }
-    }
-  })
-
-  it('Should not duplicate an HLS resolution', async function () {
-    this.timeout(120000)
-
-    await servers[0].cli.execWithEnv(`npm run create-transcoding-job -- -v ${videosUUID[2]} --generate-hls -r 480`)
-
-    await waitJobs(servers)
-
-    for (const server of servers) {
-      const videoDetails = await server.videos.get({ id: videosUUID[2] })
-
-      const files = videoDetails.streamingPlaylists[0].files
-      expect(files).to.have.lengthOf(1)
-      expect(files[0].resolution.id).to.equal(480)
-
-      if (objectStorage) await checkFilesInObjectStorage(files, 'playlist')
-    }
-  })
-
-  it('Should generate all HLS resolutions', async function () {
-    this.timeout(120000)
-
-    await servers[0].cli.execWithEnv(`npm run create-transcoding-job -- -v ${videosUUID[3]} --generate-hls`)
-
-    await waitJobs(servers)
-
-    for (const server of servers) {
-      const videoDetails = await server.videos.get({ id: videosUUID[3] })
-
-      expect(videoDetails.files).to.have.lengthOf(1)
-      expect(videoDetails.streamingPlaylists).to.have.lengthOf(1)
-
-      const files = videoDetails.streamingPlaylists[0].files
-      expect(files).to.have.lengthOf(4)
-
-      if (objectStorage) await checkFilesInObjectStorage(files, 'playlist')
-    }
-  })
-
-  it('Should optimize the video file and generate HLS videos if enabled in config', async function () {
-    this.timeout(120000)
-
-    await servers[0].config.enableTranscoding()
-    await servers[0].cli.execWithEnv(`npm run create-transcoding-job -- -v ${videosUUID[4]}`)
-
-    await waitJobs(servers)
-
-    for (const server of servers) {
-      const videoDetails = await server.videos.get({ id: videosUUID[4] })
-
-      expect(videoDetails.files).to.have.lengthOf(5)
-      expect(videoDetails.streamingPlaylists).to.have.lengthOf(1)
-      expect(videoDetails.streamingPlaylists[0].files).to.have.lengthOf(5)
-
-      if (objectStorage) {
-        await checkFilesInObjectStorage(videoDetails.files, 'webtorrent')
-        await checkFilesInObjectStorage(videoDetails.streamingPlaylists[0].files, 'playlist')
-      }
-    }
-  })
-
-  it('Should not have updated published at attributes', async function () {
-    for (const id of videosUUID) {
-      const video = await servers[0].videos.get({ id })
-
-      expect(publishedAt.some(p => video.publishedAt === p)).to.be.true
-    }
-  })
-
-  after(async function () {
-    await cleanupTests(servers)
-  })
-}
-
-describe('Test create transcoding jobs', function () {
-
-  describe('On filesystem', function () {
-    runTests(false)
-  })
-
-  describe('On object storage', function () {
-    if (areMockObjectStorageTestsDisabled()) return
-
-    runTests(true)
-  })
-})
index 6e0cbe58b2380a189a163c8fef121cec2b810305..8579be39c2022cf88ba9772d98bfa00e12677d23 100644 (file)
@@ -1,10 +1,8 @@
 // Order of the tests we want to execute
 import './create-import-video-file-job'
-import './create-transcoding-job'
 import './create-move-video-storage-job'
 import './peertube'
 import './plugins'
-import './print-transcode-command'
 import './prune-storage'
 import './regenerate-thumbnails'
 import './reset-password'
diff --git a/server/tests/cli/print-transcode-command.ts b/server/tests/cli/print-transcode-command.ts
deleted file mode 100644 (file)
index 33b6cd2..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
-
-import { expect } from 'chai'
-import { buildAbsoluteFixturePath } from '@shared/core-utils'
-import { CLICommand } from '@shared/server-commands'
-import { VideoResolution } from '../../../shared/models/videos'
-
-describe('Test print transcode jobs', function () {
-
-  it('Should print the correct command for each resolution', async function () {
-    const fixturePath = buildAbsoluteFixturePath('video_short.webm')
-
-    for (const resolution of [
-      VideoResolution.H_720P,
-      VideoResolution.H_1080P
-    ]) {
-      const command = await CLICommand.exec(`npm run print-transcode-command -- ${fixturePath} -r ${resolution}`)
-
-      expect(command).to.includes(`-vf scale=w=-2:h=${resolution}`)
-      expect(command).to.includes(`-y -acodec aac -vcodec libx264`)
-      expect(command).to.includes('-f mp4')
-      expect(command).to.includes('-movflags faststart')
-      expect(command).to.includes('-b:a 256k')
-      expect(command).to.includes('-r 25')
-      expect(command).to.includes('-level:v 3.1')
-      expect(command).to.includes('-g:v 50')
-      expect(command).to.includes(`-maxrate:v `)
-      expect(command).to.includes(`-bufsize:v `)
-    }
-  })
-})
index 51257d3d35c184758a44538e9119e9b3c7048845..386c384e6f92c4981288921ce004a0a17a693904 100644 (file)
@@ -7,11 +7,11 @@ import {
   createSingleServer,
   killallServers,
   makeActivityPubGetRequest,
-  parseTorrentVideo,
   PeerTubeServer,
   setAccessTokensToServers,
   waitJobs
 } from '@shared/server-commands'
+import { parseTorrentVideo } from '../shared'
 
 describe('Test update host scripts', function () {
   let server: PeerTubeServer
diff --git a/server/tests/fixtures/live/0-000067.ts b/server/tests/fixtures/live/0-000067.ts
new file mode 100644 (file)
index 0000000..a59f41a
Binary files /dev/null and b/server/tests/fixtures/live/0-000067.ts differ
diff --git a/server/tests/fixtures/live/0-000068.ts b/server/tests/fixtures/live/0-000068.ts
new file mode 100644 (file)
index 0000000..83dcbbb
Binary files /dev/null and b/server/tests/fixtures/live/0-000068.ts differ
diff --git a/server/tests/fixtures/live/0-000069.ts b/server/tests/fixtures/live/0-000069.ts
new file mode 100644 (file)
index 0000000..cafd4e9
Binary files /dev/null and b/server/tests/fixtures/live/0-000069.ts differ
diff --git a/server/tests/fixtures/live/0-000070.ts b/server/tests/fixtures/live/0-000070.ts
new file mode 100644 (file)
index 0000000..0936199
Binary files /dev/null and b/server/tests/fixtures/live/0-000070.ts differ
diff --git a/server/tests/fixtures/live/0.m3u8 b/server/tests/fixtures/live/0.m3u8
new file mode 100644 (file)
index 0000000..c3be19d
--- /dev/null
@@ -0,0 +1,14 @@
+#EXTM3U
+#EXT-X-VERSION:6
+#EXT-X-TARGETDURATION:2
+#EXT-X-MEDIA-SEQUENCE:68
+#EXT-X-INDEPENDENT-SEGMENTS
+#EXTINF:2.000000,
+#EXT-X-PROGRAM-DATE-TIME:2023-04-18T13:38:39.019+0200
+0-000068.ts
+#EXTINF:2.000000,
+#EXT-X-PROGRAM-DATE-TIME:2023-04-18T13:38:41.019+0200
+0-000069.ts
+#EXTINF:2.000000,
+#EXT-X-PROGRAM-DATE-TIME:2023-04-18T13:38:43.019+0200
+0-000070.
diff --git a/server/tests/fixtures/live/1-000067.ts b/server/tests/fixtures/live/1-000067.ts
new file mode 100644 (file)
index 0000000..17db8f8
Binary files /dev/null and b/server/tests/fixtures/live/1-000067.ts differ
diff --git a/server/tests/fixtures/live/1-000068.ts b/server/tests/fixtures/live/1-000068.ts
new file mode 100644 (file)
index 0000000..f7bb970
Binary files /dev/null and b/server/tests/fixtures/live/1-000068.ts differ
diff --git a/server/tests/fixtures/live/1-000069.ts b/server/tests/fixtures/live/1-000069.ts
new file mode 100644 (file)
index 0000000..64c7913
Binary files /dev/null and b/server/tests/fixtures/live/1-000069.ts differ
diff --git a/server/tests/fixtures/live/1-000070.ts b/server/tests/fixtures/live/1-000070.ts
new file mode 100644 (file)
index 0000000..a5f04f1
Binary files /dev/null and b/server/tests/fixtures/live/1-000070.ts differ
diff --git a/server/tests/fixtures/live/1.m3u8 b/server/tests/fixtures/live/1.m3u8
new file mode 100644 (file)
index 0000000..26d7fa6
--- /dev/null
@@ -0,0 +1,14 @@
+#EXTM3U
+#EXT-X-VERSION:6
+#EXT-X-TARGETDURATION:2
+#EXT-X-MEDIA-SEQUENCE:68
+#EXT-X-INDEPENDENT-SEGMENTS
+#EXTINF:2.000000,
+#EXT-X-PROGRAM-DATE-TIME:2023-04-18T13:38:39.019+0200
+1-000068.ts
+#EXTINF:2.000000,
+#EXT-X-PROGRAM-DATE-TIME:2023-04-18T13:38:41.019+0200
+1-000069.ts
+#EXTINF:2.000000,
+#EXT-X-PROGRAM-DATE-TIME:2023-04-18T13:38:43.019+0200
+1-000070.ts
diff --git a/server/tests/fixtures/live/master.m3u8 b/server/tests/fixtures/live/master.m3u8
new file mode 100644 (file)
index 0000000..7e52f33
--- /dev/null
@@ -0,0 +1,8 @@
+#EXTM3U
+#EXT-X-VERSION:6
+#EXT-X-STREAM-INF:BANDWIDTH=1287342,RESOLUTION=640x360,CODECS="avc1.64001f,mp4a.40.2"
+0.m3u8
+
+#EXT-X-STREAM-INF:BANDWIDTH=3051742,RESOLUTION=1280x720,CODECS="avc1.64001f,mp4a.40.2"
+1.m3u8
+
diff --git a/server/tests/fixtures/video_short_0p.mp4 b/server/tests/fixtures/video_short_0p.mp4
new file mode 100644 (file)
index 0000000..2069a49
Binary files /dev/null and b/server/tests/fixtures/video_short_0p.mp4 differ
diff --git a/server/tests/fixtures/video_short_144p.m3u8 b/server/tests/fixtures/video_short_144p.m3u8
new file mode 100644 (file)
index 0000000..9656862
--- /dev/null
@@ -0,0 +1,13 @@
+#EXTM3U
+#EXT-X-VERSION:7
+#EXT-X-TARGETDURATION:4
+#EXT-X-MEDIA-SEQUENCE:0
+#EXT-X-PLAYLIST-TYPE:VOD
+#EXT-X-MAP:URI="3dd13e27-1ae1-441c-9b77-48c6b95603be-144-fragmented.mp4",BYTERANGE="1375@0"
+#EXTINF:4.000000,
+#EXT-X-BYTERANGE:10518@1375
+3dd13e27-1ae1-441c-9b77-48c6b95603be-144-fragmented.mp4
+#EXTINF:1.000000,
+#EXT-X-BYTERANGE:3741@11893
+3dd13e27-1ae1-441c-9b77-48c6b95603be-144-fragmented.mp4
+#EXT-X-ENDLIST
diff --git a/server/tests/fixtures/video_short_144p.mp4 b/server/tests/fixtures/video_short_144p.mp4
new file mode 100644 (file)
index 0000000..047d43c
Binary files /dev/null and b/server/tests/fixtures/video_short_144p.mp4 differ
diff --git a/server/tests/fixtures/video_short_240p.m3u8 b/server/tests/fixtures/video_short_240p.m3u8
new file mode 100644 (file)
index 0000000..9656862
--- /dev/null
@@ -0,0 +1,13 @@
+#EXTM3U
+#EXT-X-VERSION:7
+#EXT-X-TARGETDURATION:4
+#EXT-X-MEDIA-SEQUENCE:0
+#EXT-X-PLAYLIST-TYPE:VOD
+#EXT-X-MAP:URI="3dd13e27-1ae1-441c-9b77-48c6b95603be-144-fragmented.mp4",BYTERANGE="1375@0"
+#EXTINF:4.000000,
+#EXT-X-BYTERANGE:10518@1375
+3dd13e27-1ae1-441c-9b77-48c6b95603be-144-fragmented.mp4
+#EXTINF:1.000000,
+#EXT-X-BYTERANGE:3741@11893
+3dd13e27-1ae1-441c-9b77-48c6b95603be-144-fragmented.mp4
+#EXT-X-ENDLIST
index db074940bac2905f7daa392bcb26675c883b9e37..46609e81a65b54968c8d2ecbd2608ce3bcc80c34 100644 (file)
Binary files a/server/tests/fixtures/video_short_240p.mp4 and b/server/tests/fixtures/video_short_240p.mp4 differ
diff --git a/server/tests/fixtures/video_short_360p.m3u8 b/server/tests/fixtures/video_short_360p.m3u8
new file mode 100644 (file)
index 0000000..f7072dc
--- /dev/null
@@ -0,0 +1,13 @@
+#EXTM3U
+#EXT-X-VERSION:7
+#EXT-X-TARGETDURATION:4
+#EXT-X-MEDIA-SEQUENCE:0
+#EXT-X-PLAYLIST-TYPE:VOD
+#EXT-X-MAP:URI="05c40acd-3e94-4d25-ade8-97f7ff2cf0ac-360-fragmented.mp4",BYTERANGE="1376@0"
+#EXTINF:4.000000,
+#EXT-X-BYTERANGE:19987@1376
+05c40acd-3e94-4d25-ade8-97f7ff2cf0ac-360-fragmented.mp4
+#EXTINF:1.000000,
+#EXT-X-BYTERANGE:9147@21363
+05c40acd-3e94-4d25-ade8-97f7ff2cf0ac-360-fragmented.mp4
+#EXT-X-ENDLIST
diff --git a/server/tests/fixtures/video_short_360p.mp4 b/server/tests/fixtures/video_short_360p.mp4
new file mode 100644 (file)
index 0000000..7a8189b
Binary files /dev/null and b/server/tests/fixtures/video_short_360p.mp4 differ
diff --git a/server/tests/fixtures/video_short_480p.m3u8 b/server/tests/fixtures/video_short_480p.m3u8
new file mode 100644 (file)
index 0000000..5ff30df
--- /dev/null
@@ -0,0 +1,13 @@
+#EXTM3U
+#EXT-X-VERSION:7
+#EXT-X-TARGETDURATION:4
+#EXT-X-MEDIA-SEQUENCE:0
+#EXT-X-PLAYLIST-TYPE:VOD
+#EXT-X-MAP:URI="f9377e69-d8f2-4de8-8087-ddbca6629829-480-fragmented.mp4",BYTERANGE="1376@0"
+#EXTINF:4.000000,
+#EXT-X-BYTERANGE:26042@1376
+f9377e69-d8f2-4de8-8087-ddbca6629829-480-fragmented.mp4
+#EXTINF:1.000000,
+#EXT-X-BYTERANGE:12353@27418
+f9377e69-d8f2-4de8-8087-ddbca6629829-480-fragmented.mp4
+#EXT-X-ENDLIST
diff --git a/server/tests/fixtures/video_short_480p.mp4 b/server/tests/fixtures/video_short_480p.mp4
new file mode 100644 (file)
index 0000000..e05b58b
Binary files /dev/null and b/server/tests/fixtures/video_short_480p.mp4 differ
diff --git a/server/tests/fixtures/video_short_720p.m3u8 b/server/tests/fixtures/video_short_720p.m3u8
new file mode 100644 (file)
index 0000000..7cee940
--- /dev/null
@@ -0,0 +1,13 @@
+#EXTM3U
+#EXT-X-VERSION:7
+#EXT-X-TARGETDURATION:4
+#EXT-X-MEDIA-SEQUENCE:0
+#EXT-X-PLAYLIST-TYPE:VOD
+#EXT-X-MAP:URI="c1014aa4-d1f4-4b66-927b-c23d283fcae0-720-fragmented.mp4",BYTERANGE="1356@0"
+#EXTINF:4.000000,
+#EXT-X-BYTERANGE:39260@1356
+c1014aa4-d1f4-4b66-927b-c23d283fcae0-720-fragmented.mp4
+#EXTINF:1.000000,
+#EXT-X-BYTERANGE:18493@40616
+c1014aa4-d1f4-4b66-927b-c23d283fcae0-720-fragmented.mp4
+#EXT-X-ENDLIST
diff --git a/server/tests/fixtures/video_short_720p.mp4 b/server/tests/fixtures/video_short_720p.mp4
new file mode 100644 (file)
index 0000000..35e8f69
Binary files /dev/null and b/server/tests/fixtures/video_short_720p.mp4 differ
index 1718ac424079da029bb2f92428ca1a7addb868ea..4ec1ebe67cd41d7ee2b788e03699c043eb1313e3 100644 (file)
@@ -4,6 +4,7 @@ import './misc-endpoints'
 import './feeds/'
 import './cli/'
 import './api/'
+import './peertube-runner/'
 import './plugins/'
 import './helpers/'
 import './lib/'
index e399ac5a597ea871d43eb1cfee2bfc207293e2b2..c3480dc12fa10d113a24b2be78dc26d9224928c9 100644 (file)
@@ -63,7 +63,7 @@ describe('VideoConstantManagerFactory', function () {
     it('Should be able to add a video licence constant', () => {
       const successfullyAdded = videoLicenceManager.addConstant(42, 'European Union Public Licence')
       expect(successfullyAdded).to.be.true
-      expect(videoLicenceManager.getConstantValue(42)).to.equal('European Union Public Licence')
+      expect(videoLicenceManager.getConstantValue(42 as any)).to.equal('European Union Public Licence')
     })
 
     it('Should be able to reset video licence constants', () => {
@@ -87,9 +87,9 @@ describe('VideoConstantManagerFactory', function () {
     })
 
     it('Should be able to add a video playlist privacy constant', () => {
-      const successfullyAdded = playlistPrivacyManager.addConstant(42, 'Friends only')
+      const successfullyAdded = playlistPrivacyManager.addConstant(42 as any, 'Friends only')
       expect(successfullyAdded).to.be.true
-      expect(playlistPrivacyManager.getConstantValue(42)).to.equal('Friends only')
+      expect(playlistPrivacyManager.getConstantValue(42 as any)).to.equal('Friends only')
     })
 
     it('Should be able to reset video playlist privacy constants', () => {
@@ -113,9 +113,9 @@ describe('VideoConstantManagerFactory', function () {
     })
 
     it('Should be able to add a video privacy constant', () => {
-      const successfullyAdded = videoPrivacyManager.addConstant(42, 'Friends only')
+      const successfullyAdded = videoPrivacyManager.addConstant(42 as any, 'Friends only')
       expect(successfullyAdded).to.be.true
-      expect(videoPrivacyManager.getConstantValue(42)).to.equal('Friends only')
+      expect(videoPrivacyManager.getConstantValue(42 as any)).to.equal('Friends only')
     })
 
     it('Should be able to reset video privacy constants', () => {
diff --git a/server/tests/peertube-runner/client-cli.ts b/server/tests/peertube-runner/client-cli.ts
new file mode 100644 (file)
index 0000000..90bf73e
--- /dev/null
@@ -0,0 +1,71 @@
+/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
+
+import { expect } from 'chai'
+import { PeerTubeRunnerProcess } from '@server/tests/shared'
+import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServers, setDefaultVideoChannel } from '@shared/server-commands'
+
+describe('Test peertube-runner program client CLI', function () {
+  let server: PeerTubeServer
+  let peertubeRunner: PeerTubeRunnerProcess
+
+  before(async function () {
+    this.timeout(120_000)
+
+    server = await createSingleServer(1)
+
+    await setAccessTokensToServers([ server ])
+    await setDefaultVideoChannel([ server ])
+
+    await server.config.enableRemoteTranscoding()
+
+    peertubeRunner = new PeerTubeRunnerProcess()
+    await peertubeRunner.runServer()
+  })
+
+  it('Should not have PeerTube instance listed', async function () {
+    const data = await peertubeRunner.listRegisteredPeerTubeInstances()
+
+    expect(data).to.not.contain(server.url)
+  })
+
+  it('Should register a new PeerTube instance', async function () {
+    const registrationToken = await server.runnerRegistrationTokens.getFirstRegistrationToken()
+
+    await peertubeRunner.registerPeerTubeInstance({
+      server,
+      registrationToken,
+      runnerName: 'my super runner',
+      runnerDescription: 'super description'
+    })
+  })
+
+  it('Should list this new PeerTube instance', async function () {
+    const data = await peertubeRunner.listRegisteredPeerTubeInstances()
+
+    expect(data).to.contain(server.url)
+    expect(data).to.contain('my super runner')
+    expect(data).to.contain('super description')
+  })
+
+  it('Should still have the configuration after a restart', async function () {
+    peertubeRunner.kill()
+
+    await peertubeRunner.runServer()
+  })
+
+  it('Should unregister the PeerTube instance', async function () {
+    await peertubeRunner.unregisterPeerTubeInstance({ server })
+  })
+
+  it('Should not have PeerTube instance listed', async function () {
+    const data = await peertubeRunner.listRegisteredPeerTubeInstances()
+
+    expect(data).to.not.contain(server.url)
+  })
+
+  after(async function () {
+    await cleanupTests([ server ])
+
+    peertubeRunner.kill()
+  })
+})
diff --git a/server/tests/peertube-runner/index.ts b/server/tests/peertube-runner/index.ts
new file mode 100644 (file)
index 0000000..6258d6e
--- /dev/null
@@ -0,0 +1,3 @@
+export * from './client-cli'
+export * from './live-transcoding'
+export * from './vod-transcoding'
diff --git a/server/tests/peertube-runner/live-transcoding.ts b/server/tests/peertube-runner/live-transcoding.ts
new file mode 100644 (file)
index 0000000..f58e920
--- /dev/null
@@ -0,0 +1,178 @@
+import { expect } from 'chai'
+/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
+import { expectStartWith, PeerTubeRunnerProcess, SQLCommand, testLiveVideoResolutions } from '@server/tests/shared'
+import { areMockObjectStorageTestsDisabled, wait } from '@shared/core-utils'
+import { HttpStatusCode, VideoPrivacy } from '@shared/models'
+import {
+  cleanupTests,
+  createMultipleServers,
+  doubleFollow,
+  findExternalSavedVideo,
+  makeRawRequest,
+  ObjectStorageCommand,
+  PeerTubeServer,
+  setAccessTokensToServers,
+  setDefaultVideoChannel,
+  stopFfmpeg,
+  waitJobs,
+  waitUntilLivePublishedOnAllServers,
+  waitUntilLiveWaitingOnAllServers
+} from '@shared/server-commands'
+
+describe('Test Live transcoding in peertube-runner program', function () {
+  let servers: PeerTubeServer[] = []
+  let peertubeRunner: PeerTubeRunnerProcess
+  let sqlCommandServer1: SQLCommand
+
+  function runSuite (options: {
+    objectStorage: boolean
+  }) {
+    const { objectStorage } = options
+
+    it('Should enable transcoding without additional resolutions', async function () {
+      this.timeout(120000)
+
+      const { video } = await servers[0].live.quickCreate({ permanentLive: true, saveReplay: false, privacy: VideoPrivacy.PUBLIC })
+
+      const ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: video.uuid })
+      await waitUntilLivePublishedOnAllServers(servers, video.uuid)
+      await waitJobs(servers)
+
+      await testLiveVideoResolutions({
+        originServer: servers[0],
+        sqlCommand: sqlCommandServer1,
+        servers,
+        liveVideoId: video.uuid,
+        resolutions: [ 720, 480, 360, 240, 144 ],
+        objectStorage,
+        transcoded: true
+      })
+
+      await stopFfmpeg(ffmpegCommand)
+
+      await waitUntilLiveWaitingOnAllServers(servers, video.uuid)
+      await servers[0].videos.remove({ id: video.id })
+    })
+
+    it('Should transcode audio only RTMP stream', async function () {
+      this.timeout(120000)
+
+      const { video } = await servers[0].live.quickCreate({ permanentLive: true, saveReplay: false, privacy: VideoPrivacy.UNLISTED })
+
+      const ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: video.uuid, fixtureName: 'video_short_no_audio.mp4' })
+      await waitUntilLivePublishedOnAllServers(servers, video.uuid)
+      await waitJobs(servers)
+
+      await stopFfmpeg(ffmpegCommand)
+
+      await waitUntilLiveWaitingOnAllServers(servers, video.uuid)
+      await servers[0].videos.remove({ id: video.id })
+    })
+
+    it('Should save a replay', async function () {
+      this.timeout(120000)
+
+      const { video } = await servers[0].live.quickCreate({ permanentLive: true, saveReplay: true })
+
+      const ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: video.uuid })
+      await waitUntilLivePublishedOnAllServers(servers, video.uuid)
+
+      await testLiveVideoResolutions({
+        originServer: servers[0],
+        sqlCommand: sqlCommandServer1,
+        servers,
+        liveVideoId: video.uuid,
+        resolutions: [ 720, 480, 360, 240, 144 ],
+        objectStorage,
+        transcoded: true
+      })
+
+      await stopFfmpeg(ffmpegCommand)
+
+      await waitUntilLiveWaitingOnAllServers(servers, video.uuid)
+      await waitJobs(servers)
+
+      const session = await servers[0].live.findLatestSession({ videoId: video.uuid })
+      expect(session.endingProcessed).to.be.true
+      expect(session.endDate).to.exist
+      expect(session.saveReplay).to.be.true
+
+      const videoLiveDetails = await servers[0].videos.get({ id: video.uuid })
+      const replay = await findExternalSavedVideo(servers[0], videoLiveDetails)
+
+      for (const server of servers) {
+        const video = await server.videos.get({ id: replay.uuid })
+
+        expect(video.files).to.have.lengthOf(0)
+        expect(video.streamingPlaylists).to.have.lengthOf(1)
+
+        const files = video.streamingPlaylists[0].files
+        expect(files).to.have.lengthOf(5)
+
+        for (const file of files) {
+          if (objectStorage) {
+            expectStartWith(file.fileUrl, ObjectStorageCommand.getMockPlaylistBaseUrl())
+          }
+
+          await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 })
+        }
+      }
+    })
+  }
+
+  before(async function () {
+    this.timeout(120_000)
+
+    servers = await createMultipleServers(2)
+
+    await setAccessTokensToServers(servers)
+    await setDefaultVideoChannel(servers)
+
+    await doubleFollow(servers[0], servers[1])
+
+    sqlCommandServer1 = new SQLCommand(servers[0])
+
+    await servers[0].config.enableRemoteTranscoding()
+    await servers[0].config.enableTranscoding(true, true, true)
+    await servers[0].config.enableLive({ allowReplay: true, resolutions: 'max', transcoding: true })
+
+    const registrationToken = await servers[0].runnerRegistrationTokens.getFirstRegistrationToken()
+
+    peertubeRunner = new PeerTubeRunnerProcess()
+    await peertubeRunner.runServer({ hideLogs: false })
+    await peertubeRunner.registerPeerTubeInstance({ server: servers[0], registrationToken, runnerName: 'runner' })
+  })
+
+  describe('With lives on local filesystem storage', function () {
+
+    before(async function () {
+      await servers[0].config.enableTranscoding(true, false, true)
+    })
+
+    runSuite({ objectStorage: false })
+  })
+
+  describe('With lives on object storage', function () {
+    if (areMockObjectStorageTestsDisabled()) return
+
+    before(async function () {
+      await ObjectStorageCommand.prepareDefaultMockBuckets()
+
+      await servers[0].kill()
+
+      await servers[0].run(ObjectStorageCommand.getDefaultMockConfig())
+
+      // Wait for peertube runner socket reconnection
+      await wait(1500)
+    })
+
+    runSuite({ objectStorage: true })
+  })
+
+  after(async function () {
+    await peertubeRunner.unregisterPeerTubeInstance({ server: servers[0] })
+    peertubeRunner.kill()
+
+    await cleanupTests(servers)
+  })
+})
diff --git a/server/tests/peertube-runner/vod-transcoding.ts b/server/tests/peertube-runner/vod-transcoding.ts
new file mode 100644 (file)
index 0000000..bdf7983
--- /dev/null
@@ -0,0 +1,330 @@
+/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
+import { expect } from 'chai'
+import { completeCheckHlsPlaylist, completeWebVideoFilesCheck, PeerTubeRunnerProcess } from '@server/tests/shared'
+import { areMockObjectStorageTestsDisabled, getAllFiles, wait } from '@shared/core-utils'
+import { VideoPrivacy } from '@shared/models'
+import {
+  cleanupTests,
+  createMultipleServers,
+  doubleFollow,
+  ObjectStorageCommand,
+  PeerTubeServer,
+  setAccessTokensToServers,
+  setDefaultVideoChannel,
+  waitJobs
+} from '@shared/server-commands'
+
+describe('Test VOD transcoding in peertube-runner program', function () {
+  let servers: PeerTubeServer[] = []
+  let peertubeRunner: PeerTubeRunnerProcess
+
+  function runSuite (options: {
+    webtorrentEnabled: boolean
+    hlsEnabled: boolean
+    objectStorage: boolean
+  }) {
+    const { webtorrentEnabled, hlsEnabled, objectStorage } = options
+
+    const objectStorageBaseUrlWebTorrent = objectStorage
+      ? ObjectStorageCommand.getMockWebTorrentBaseUrl()
+      : undefined
+
+    const objectStorageBaseUrlHLS = objectStorage
+      ? ObjectStorageCommand.getMockPlaylistBaseUrl()
+      : undefined
+
+    it('Should upload a classic video mp4 and transcode it', async function () {
+      this.timeout(120000)
+
+      const { uuid } = await servers[0].videos.quickUpload({ name: 'mp4', fixture: 'video_short.mp4' })
+
+      await waitJobs(servers, { runnerJobs: true })
+
+      for (const server of servers) {
+        if (webtorrentEnabled) {
+          await completeWebVideoFilesCheck({
+            server,
+            originServer: servers[0],
+            fixture: 'video_short.mp4',
+            videoUUID: uuid,
+            objectStorageBaseUrl: objectStorageBaseUrlWebTorrent,
+            files: [
+              { resolution: 0 },
+              { resolution: 144 },
+              { resolution: 240 },
+              { resolution: 360 },
+              { resolution: 480 },
+              { resolution: 720 }
+            ]
+          })
+        }
+
+        if (hlsEnabled) {
+          await completeCheckHlsPlaylist({
+            hlsOnly: !webtorrentEnabled,
+            servers,
+            videoUUID: uuid,
+            objectStorageBaseUrl: objectStorageBaseUrlHLS,
+            resolutions: [ 720, 480, 360, 240, 144, 0 ]
+          })
+        }
+      }
+    })
+
+    it('Should upload a webm video and transcode it', async function () {
+      this.timeout(120000)
+
+      const { uuid } = await servers[0].videos.quickUpload({ name: 'mp4', fixture: 'video_short.webm' })
+
+      await waitJobs(servers, { runnerJobs: true })
+
+      for (const server of servers) {
+        if (webtorrentEnabled) {
+          await completeWebVideoFilesCheck({
+            server,
+            originServer: servers[0],
+            fixture: 'video_short.webm',
+            videoUUID: uuid,
+            objectStorageBaseUrl: objectStorageBaseUrlWebTorrent,
+            files: [
+              { resolution: 0 },
+              { resolution: 144 },
+              { resolution: 240 },
+              { resolution: 360 },
+              { resolution: 480 },
+              { resolution: 720 }
+            ]
+          })
+        }
+
+        if (hlsEnabled) {
+          await completeCheckHlsPlaylist({
+            hlsOnly: !webtorrentEnabled,
+            servers,
+            videoUUID: uuid,
+            objectStorageBaseUrl: objectStorageBaseUrlHLS,
+            resolutions: [ 720, 480, 360, 240, 144, 0 ]
+          })
+        }
+      }
+    })
+
+    it('Should upload an audio only video and transcode it', async function () {
+      this.timeout(120000)
+
+      const attributes = { name: 'audio_without_preview', fixture: 'sample.ogg' }
+      const { uuid } = await servers[0].videos.upload({ attributes, mode: 'resumable' })
+
+      await waitJobs(servers, { runnerJobs: true })
+
+      for (const server of servers) {
+        if (webtorrentEnabled) {
+          await completeWebVideoFilesCheck({
+            server,
+            originServer: servers[0],
+            fixture: 'sample.ogg',
+            videoUUID: uuid,
+            objectStorageBaseUrl: objectStorageBaseUrlWebTorrent,
+            files: [
+              { resolution: 0 },
+              { resolution: 144 },
+              { resolution: 240 },
+              { resolution: 360 },
+              { resolution: 480 }
+            ]
+          })
+        }
+
+        if (hlsEnabled) {
+          await completeCheckHlsPlaylist({
+            hlsOnly: !webtorrentEnabled,
+            servers,
+            videoUUID: uuid,
+            objectStorageBaseUrl: objectStorageBaseUrlHLS,
+            resolutions: [ 480, 360, 240, 144, 0 ]
+          })
+        }
+      }
+    })
+
+    it('Should upload a private video and transcode it', async function () {
+      this.timeout(120000)
+
+      const { uuid } = await servers[0].videos.quickUpload({ name: 'mp4', fixture: 'video_short.mp4', privacy: VideoPrivacy.PRIVATE })
+
+      await waitJobs(servers, { runnerJobs: true })
+
+      if (webtorrentEnabled) {
+        await completeWebVideoFilesCheck({
+          server: servers[0],
+          originServer: servers[0],
+          fixture: 'video_short.mp4',
+          videoUUID: uuid,
+          objectStorageBaseUrl: objectStorageBaseUrlWebTorrent,
+          files: [
+            { resolution: 0 },
+            { resolution: 144 },
+            { resolution: 240 },
+            { resolution: 360 },
+            { resolution: 480 },
+            { resolution: 720 }
+          ]
+        })
+      }
+
+      if (hlsEnabled) {
+        await completeCheckHlsPlaylist({
+          hlsOnly: !webtorrentEnabled,
+          servers: [ servers[0] ],
+          videoUUID: uuid,
+          objectStorageBaseUrl: objectStorageBaseUrlHLS,
+          resolutions: [ 720, 480, 360, 240, 144, 0 ]
+        })
+      }
+    })
+
+    it('Should transcode videos on manual run', async function () {
+      this.timeout(120000)
+
+      await servers[0].config.disableTranscoding()
+
+      const { uuid } = await servers[0].videos.quickUpload({ name: 'manual transcoding', fixture: 'video_short.mp4' })
+      await waitJobs(servers, { runnerJobs: true })
+
+      {
+        const video = await servers[0].videos.get({ id: uuid })
+        expect(getAllFiles(video)).to.have.lengthOf(1)
+      }
+
+      await servers[0].config.enableTranscoding(true, true, true)
+
+      await servers[0].videos.runTranscoding({ transcodingType: 'webtorrent', videoId: uuid })
+      await waitJobs(servers, { runnerJobs: true })
+
+      await completeWebVideoFilesCheck({
+        server: servers[0],
+        originServer: servers[0],
+        fixture: 'video_short.mp4',
+        videoUUID: uuid,
+        objectStorageBaseUrl: objectStorageBaseUrlWebTorrent,
+        files: [
+          { resolution: 0 },
+          { resolution: 144 },
+          { resolution: 240 },
+          { resolution: 360 },
+          { resolution: 480 },
+          { resolution: 720 }
+        ]
+      })
+
+      await servers[0].videos.runTranscoding({ transcodingType: 'hls', videoId: uuid })
+      await waitJobs(servers, { runnerJobs: true })
+
+      await completeCheckHlsPlaylist({
+        hlsOnly: false,
+        servers: [ servers[0] ],
+        videoUUID: uuid,
+        objectStorageBaseUrl: objectStorageBaseUrlHLS,
+        resolutions: [ 720, 480, 360, 240, 144, 0 ]
+      })
+    })
+  }
+
+  before(async function () {
+    this.timeout(120_000)
+
+    servers = await createMultipleServers(2)
+
+    await setAccessTokensToServers(servers)
+    await setDefaultVideoChannel(servers)
+
+    await doubleFollow(servers[0], servers[1])
+
+    await servers[0].config.enableRemoteTranscoding()
+
+    const registrationToken = await servers[0].runnerRegistrationTokens.getFirstRegistrationToken()
+
+    peertubeRunner = new PeerTubeRunnerProcess()
+    await peertubeRunner.runServer()
+    await peertubeRunner.registerPeerTubeInstance({ server: servers[0], registrationToken, runnerName: 'runner' })
+  })
+
+  describe('With videos on local filesystem storage', function () {
+
+    describe('Web video only enabled', function () {
+
+      before(async function () {
+        await servers[0].config.enableTranscoding(true, false, true)
+      })
+
+      runSuite({ webtorrentEnabled: true, hlsEnabled: false, objectStorage: false })
+    })
+
+    describe('HLS videos only enabled', function () {
+
+      before(async function () {
+        await servers[0].config.enableTranscoding(false, true, true)
+      })
+
+      runSuite({ webtorrentEnabled: false, hlsEnabled: true, objectStorage: false })
+    })
+
+    describe('Web video & HLS enabled', function () {
+
+      before(async function () {
+        await servers[0].config.enableTranscoding(true, true, true)
+      })
+
+      runSuite({ webtorrentEnabled: true, hlsEnabled: true, objectStorage: false })
+    })
+  })
+
+  describe('With videos on object storage', function () {
+    if (areMockObjectStorageTestsDisabled()) return
+
+    before(async function () {
+      await ObjectStorageCommand.prepareDefaultMockBuckets()
+
+      await servers[0].kill()
+
+      await servers[0].run(ObjectStorageCommand.getDefaultMockConfig())
+
+      // Wait for peertube runner socket reconnection
+      await wait(1500)
+    })
+
+    describe('Web video only enabled', function () {
+
+      before(async function () {
+        await servers[0].config.enableTranscoding(true, false, true)
+      })
+
+      runSuite({ webtorrentEnabled: true, hlsEnabled: false, objectStorage: true })
+    })
+
+    describe('HLS videos only enabled', function () {
+
+      before(async function () {
+        await servers[0].config.enableTranscoding(false, true, true)
+      })
+
+      runSuite({ webtorrentEnabled: false, hlsEnabled: true, objectStorage: true })
+    })
+
+    describe('Web video & HLS enabled', function () {
+
+      before(async function () {
+        await servers[0].config.enableTranscoding(true, true, true)
+      })
+
+      runSuite({ webtorrentEnabled: true, hlsEnabled: true, objectStorage: true })
+    })
+  })
+
+  after(async function () {
+    await peertubeRunner.unregisterPeerTubeInstance({ server: servers[0] })
+    peertubeRunner.kill()
+
+    await cleanupTests(servers)
+  })
+})
index ce1047388383176290e3133bfeaee3ebb898f418..689eec5acdb3baafdf0b3475fb9d12c7b6684176 100644 (file)
@@ -1,7 +1,7 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
 import { expect } from 'chai'
-import { getAudioStream, getVideoStreamFPS, getVideoStream } from '@server/helpers/ffmpeg'
+import { getAudioStream, getVideoStream, getVideoStreamFPS } from '@shared/ffmpeg'
 import { VideoPrivacy } from '@shared/models'
 import {
   cleanupTests,
index c0098b293851a50299fab61647193dcd60a3b4f7..d7eb25bb54ccfbbae67a5801781561a9bd8ff9d4 100644 (file)
@@ -11,7 +11,7 @@ import { HttpStatusCode } from '@shared/models'
 import { makeGetRequest, PeerTubeServer } from '@shared/server-commands'
 
 // Default interval -> 5 minutes
-function dateIsValid (dateString: string, interval = 300000) {
+function dateIsValid (dateString: string | Date, interval = 300000) {
   const dateToCheck = new Date(dateString)
   const now = new Date()
 
@@ -90,6 +90,8 @@ async function testFileExistsOrNot (server: PeerTubeServer, directory: string, f
   expect(await pathExists(join(base, filePath))).to.equal(exist)
 }
 
+// ---------------------------------------------------------------------------
+
 function checkBadStartPagination (url: string, path: string, token?: string, query = {}) {
   return makeGetRequest({
     url,
index 9a57084e45dc408aaced4eff61a56650bc17106c..b0c8dba66972b9147025bc63807d24cd68265621 100644 (file)
@@ -3,7 +3,7 @@ import ffmpeg from 'fluent-ffmpeg'
 import { ensureDir, pathExists } from 'fs-extra'
 import { dirname } from 'path'
 import { buildAbsoluteFixturePath, getMaxBitrate } from '@shared/core-utils'
-import { getVideoStreamBitrate, getVideoStreamFPS, getVideoStreamDimensionsInfo } from '@shared/extra-utils'
+import { getVideoStreamBitrate, getVideoStreamDimensionsInfo, getVideoStreamFPS } from '@shared/ffmpeg'
 
 async function ensureHasTooBigBitrate (fixturePath: string) {
   const bitrate = await getVideoStreamBitrate(fixturePath)
index 963ef8fe6e2c36ae46a991c4402b7f4e5d19c74e..eda24adb50483f70b4544e4e71ef9fadee93e32f 100644 (file)
@@ -6,11 +6,14 @@ export * from './directories'
 export * from './generate'
 export * from './live'
 export * from './notifications'
+export * from './peertube-runner-process'
 export * from './video-playlists'
 export * from './plugins'
 export * from './requests'
+export * from './sql-command'
 export * from './streaming-playlists'
 export * from './tests'
 export * from './tracker'
 export * from './videos'
 export * from './views'
+export * from './webtorrent'
index ff0b2f226f434d66c9ff6f9414dc581aeed50850..31f92ef19a568b5c5396d58d31eba97784b9924a 100644 (file)
@@ -6,6 +6,7 @@ import { join } from 'path'
 import { sha1 } from '@shared/extra-utils'
 import { LiveVideo, VideoStreamingPlaylistType } from '@shared/models'
 import { ObjectStorageCommand, PeerTubeServer } from '@shared/server-commands'
+import { SQLCommand } from './sql-command'
 import { checkLiveSegmentHash, checkResolutionsInMasterPlaylist } from './streaming-playlists'
 
 async function checkLiveCleanup (options: {
@@ -36,8 +37,10 @@ async function checkLiveCleanup (options: {
 
 // ---------------------------------------------------------------------------
 
-async function testVideoResolutions (options: {
+async function testLiveVideoResolutions (options: {
+  sqlCommand: SQLCommand
   originServer: PeerTubeServer
+
   servers: PeerTubeServer[]
   liveVideoId: string
   resolutions: number[]
@@ -48,6 +51,7 @@ async function testVideoResolutions (options: {
 }) {
   const {
     originServer,
+    sqlCommand,
     servers,
     liveVideoId,
     resolutions,
@@ -116,7 +120,7 @@ async function testVideoResolutions (options: {
 
       if (originServer.internalServerNumber === server.internalServerNumber) {
         const infohash = sha1(`${2 + hlsPlaylist.playlistUrl}+V${i}`)
-        const dbInfohashes = await originServer.sql.getPlaylistInfohash(hlsPlaylist.id)
+        const dbInfohashes = await sqlCommand.getPlaylistInfohash(hlsPlaylist.id)
 
         expect(dbInfohashes).to.include(infohash)
       }
@@ -128,7 +132,7 @@ async function testVideoResolutions (options: {
 
 export {
   checkLiveCleanup,
-  testVideoResolutions
+  testLiveVideoResolutions
 }
 
 // ---------------------------------------------------------------------------
diff --git a/server/tests/shared/peertube-runner-process.ts b/server/tests/shared/peertube-runner-process.ts
new file mode 100644 (file)
index 0000000..84e2dc6
--- /dev/null
@@ -0,0 +1,87 @@
+import { ChildProcess, fork } from 'child_process'
+import execa from 'execa'
+import { join } from 'path'
+import { root } from '@shared/core-utils'
+import { PeerTubeServer } from '@shared/server-commands'
+
+export class PeerTubeRunnerProcess {
+  private app?: ChildProcess
+
+  runServer (options: {
+    hideLogs?: boolean // default true
+  } = {}) {
+    const { hideLogs = true } = options
+
+    return new Promise<void>((res, rej) => {
+      const args = [ 'server', '--verbose', '--id', 'test' ]
+
+      const forkOptions = {
+        detached: false,
+        silent: true
+      }
+      this.app = fork(this.getRunnerPath(), args, forkOptions)
+
+      this.app.stdout.on('data', data => {
+        const str = data.toString() as string
+
+        if (!hideLogs) {
+          console.log(str)
+        }
+      })
+
+      res()
+    })
+  }
+
+  registerPeerTubeInstance (options: {
+    server: PeerTubeServer
+    registrationToken: string
+    runnerName: string
+    runnerDescription?: string
+  }) {
+    const { server, registrationToken, runnerName, runnerDescription } = options
+
+    const args = [
+      'register',
+      '--url', server.url,
+      '--registration-token', registrationToken,
+      '--runner-name', runnerName,
+      '--id', 'test'
+    ]
+
+    if (runnerDescription) {
+      args.push('--runner-description')
+      args.push(runnerDescription)
+    }
+
+    return execa.node(this.getRunnerPath(), args)
+  }
+
+  unregisterPeerTubeInstance (options: {
+    server: PeerTubeServer
+  }) {
+    const { server } = options
+
+    const args = [ 'unregister', '--url', server.url, '--id', 'test' ]
+    return execa.node(this.getRunnerPath(), args)
+  }
+
+  async listRegisteredPeerTubeInstances () {
+    const args = [ 'list-registered', '--id', 'test' ]
+    const { stdout } = await execa.node(this.getRunnerPath(), args)
+
+    return stdout
+  }
+
+  kill () {
+    if (!this.app) return
+
+    process.kill(this.app.pid)
+
+    this.app = null
+  }
+
+  private getRunnerPath () {
+    return join(root(), 'packages', 'peertube-runner', 'dist', 'peertube-runner.js')
+  }
+}
similarity index 96%
rename from shared/server-commands/miscs/sql-command.ts
rename to server/tests/shared/sql-command.ts
index 35cc2253f7d09ae2cff85637d9f13212fee29a16..5c53a8ac650dbc07fd1946268436ce8017a1ff74 100644 (file)
@@ -1,10 +1,14 @@
 import { QueryTypes, Sequelize } from 'sequelize'
 import { forceNumber } from '@shared/core-utils'
-import { AbstractCommand } from '../shared'
+import { PeerTubeServer } from '@shared/server-commands'
 
-export class SQLCommand extends AbstractCommand {
+export class SQLCommand {
   private sequelize: Sequelize
 
+  constructor (private readonly server: PeerTubeServer) {
+
+  }
+
   deleteAll (table: string) {
     const seq = this.getSequelize()
 
index 1c38cb512ef99695add6b746f8a8dbcc54ee2898..acfb2b4085d54a8a2876304bfee8e35a81728cf2 100644 (file)
@@ -4,10 +4,11 @@ import { expect } from 'chai'
 import { basename, dirname, join } from 'path'
 import { removeFragmentedMP4Ext, uuidRegex } from '@shared/core-utils'
 import { sha256 } from '@shared/extra-utils'
-import { HttpStatusCode, VideoStreamingPlaylist, VideoStreamingPlaylistType } from '@shared/models'
-import { makeRawRequest, PeerTubeServer, webtorrentAdd } from '@shared/server-commands'
+import { HttpStatusCode, VideoPrivacy, VideoResolution, VideoStreamingPlaylist, VideoStreamingPlaylistType } from '@shared/models'
+import { makeRawRequest, PeerTubeServer } from '@shared/server-commands'
 import { expectStartWith } from './checks'
 import { hlsInfohashExist } from './tracker'
+import { checkWebTorrentWorks } from './webtorrent'
 
 async function checkSegmentHash (options: {
   server: PeerTubeServer
@@ -15,14 +16,15 @@ async function checkSegmentHash (options: {
   baseUrlSegment: string
   resolution: number
   hlsPlaylist: VideoStreamingPlaylist
+  token?: string
 }) {
-  const { server, baseUrlPlaylist, baseUrlSegment, resolution, hlsPlaylist } = options
+  const { server, baseUrlPlaylist, baseUrlSegment, resolution, hlsPlaylist, token } = options
   const command = server.streamingPlaylists
 
   const file = hlsPlaylist.files.find(f => f.resolution.id === resolution)
   const videoName = basename(file.fileUrl)
 
-  const playlist = await command.get({ url: `${baseUrlPlaylist}/${removeFragmentedMP4Ext(videoName)}.m3u8` })
+  const playlist = await command.get({ url: `${baseUrlPlaylist}/${removeFragmentedMP4Ext(videoName)}.m3u8`, token })
 
   const matches = /#EXT-X-BYTERANGE:(\d+)@(\d+)/.exec(playlist)
 
@@ -33,11 +35,12 @@ async function checkSegmentHash (options: {
   const segmentBody = await command.getFragmentedSegment({
     url: `${baseUrlSegment}/${videoName}`,
     expectedStatus: HttpStatusCode.PARTIAL_CONTENT_206,
-    range: `bytes=${range}`
+    range: `bytes=${range}`,
+    token
   })
 
-  const shaBody = await command.getSegmentSha256({ url: hlsPlaylist.segmentsSha256Url })
-  expect(sha256(segmentBody)).to.equal(shaBody[videoName][range])
+  const shaBody = await command.getSegmentSha256({ url: hlsPlaylist.segmentsSha256Url, token })
+  expect(sha256(segmentBody)).to.equal(shaBody[videoName][range], `Invalid sha256 result for ${videoName} range ${range}`)
 }
 
 // ---------------------------------------------------------------------------
@@ -64,19 +67,24 @@ async function checkResolutionsInMasterPlaylist (options: {
   server: PeerTubeServer
   playlistUrl: string
   resolutions: number[]
+  token?: string
   transcoded?: boolean // default true
   withRetry?: boolean // default false
 }) {
-  const { server, playlistUrl, resolutions, withRetry = false, transcoded = true } = options
+  const { server, playlistUrl, resolutions, token, withRetry = false, transcoded = true } = options
 
-  const masterPlaylist = await server.streamingPlaylists.get({ url: playlistUrl, withRetry })
+  const masterPlaylist = await server.streamingPlaylists.get({ url: playlistUrl, token, withRetry })
 
   for (const resolution of resolutions) {
-    const reg = transcoded
-      ? new RegExp('#EXT-X-STREAM-INF:BANDWIDTH=\\d+,RESOLUTION=\\d+x' + resolution + ',(FRAME-RATE=\\d+,)?CODECS="avc1.64001f,mp4a.40.2"')
-      : new RegExp('#EXT-X-STREAM-INF:BANDWIDTH=\\d+,RESOLUTION=\\d+x' + resolution + '')
-
-    expect(masterPlaylist).to.match(reg)
+    const base = '#EXT-X-STREAM-INF:BANDWIDTH=\\d+,RESOLUTION=\\d+x' + resolution
+
+    if (resolution === VideoResolution.H_NOVIDEO) {
+      expect(masterPlaylist).to.match(new RegExp(`${base},CODECS="mp4a.40.2"`))
+    } else if (transcoded) {
+      expect(masterPlaylist).to.match(new RegExp(`${base},(FRAME-RATE=\\d+,)?CODECS="avc1.64001f,mp4a.40.2"`))
+    } else {
+      expect(masterPlaylist).to.match(new RegExp(`${base}`))
+    }
   }
 
   const playlistsLength = masterPlaylist.split('\n').filter(line => line.startsWith('#EXT-X-STREAM-INF:BANDWIDTH='))
@@ -89,14 +97,23 @@ async function completeCheckHlsPlaylist (options: {
   hlsOnly: boolean
 
   resolutions?: number[]
-  objectStorageBaseUrl: string
+  objectStorageBaseUrl?: string
 }) {
   const { videoUUID, hlsOnly, objectStorageBaseUrl } = options
 
   const resolutions = options.resolutions ?? [ 240, 360, 480, 720 ]
 
   for (const server of options.servers) {
-    const videoDetails = await server.videos.get({ id: videoUUID })
+    const videoDetails = await server.videos.getWithToken({ id: videoUUID })
+    const requiresAuth = videoDetails.privacy.id === VideoPrivacy.PRIVATE || videoDetails.privacy.id === VideoPrivacy.INTERNAL
+
+    const privatePath = requiresAuth
+      ? 'private/'
+      : ''
+    const token = requiresAuth
+      ? server.accessToken
+      : undefined
+
     const baseUrl = `http://${videoDetails.account.host}`
 
     expect(videoDetails.streamingPlaylists).to.have.lengthOf(1)
@@ -115,35 +132,55 @@ async function completeCheckHlsPlaylist (options: {
       const file = hlsFiles.find(f => f.resolution.id === resolution)
       expect(file).to.not.be.undefined
 
-      expect(file.magnetUri).to.have.lengthOf.above(2)
-      expect(file.torrentUrl).to.match(
-        new RegExp(`${server.url}/lazy-static/torrents/${uuidRegex}-${file.resolution.id}-hls.torrent`)
-      )
-
-      if (objectStorageBaseUrl) {
-        expectStartWith(file.fileUrl, objectStorageBaseUrl)
+      if (file.resolution.id === VideoResolution.H_NOVIDEO) {
+        expect(file.resolution.label).to.equal('Audio')
       } else {
-        expect(file.fileUrl).to.match(
-          new RegExp(`${baseUrl}/static/streaming-playlists/hls/${videoDetails.uuid}/${uuidRegex}-${file.resolution.id}-fragmented.mp4`)
-        )
+        expect(file.resolution.label).to.equal(resolution + 'p')
       }
 
-      expect(file.resolution.label).to.equal(resolution + 'p')
-
-      await makeRawRequest({ url: file.torrentUrl, expectedStatus: HttpStatusCode.OK_200 })
-      await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 })
+      expect(file.magnetUri).to.have.lengthOf.above(2)
+      await checkWebTorrentWorks(file.magnetUri)
+
+      {
+        const nameReg = `${uuidRegex}-${file.resolution.id}`
+
+        expect(file.torrentUrl).to.match(new RegExp(`${server.url}/lazy-static/torrents/${nameReg}-hls.torrent`))
+
+        if (objectStorageBaseUrl && requiresAuth) {
+          // eslint-disable-next-line max-len
+          expect(file.fileUrl).to.match(new RegExp(`${server.url}/object-storage-proxy/streaming-playlists/hls/${privatePath}${videoDetails.uuid}/${nameReg}-fragmented.mp4`))
+        } else if (objectStorageBaseUrl) {
+          expectStartWith(file.fileUrl, objectStorageBaseUrl)
+        } else {
+          expect(file.fileUrl).to.match(
+            new RegExp(`${baseUrl}/static/streaming-playlists/hls/${privatePath}${videoDetails.uuid}/${nameReg}-fragmented.mp4`)
+          )
+        }
+      }
 
-      const torrent = await webtorrentAdd(file.magnetUri, true)
-      expect(torrent.files).to.be.an('array')
-      expect(torrent.files.length).to.equal(1)
-      expect(torrent.files[0].path).to.exist.and.to.not.equal('')
+      {
+        await Promise.all([
+          makeRawRequest({ url: file.torrentUrl, token, expectedStatus: HttpStatusCode.OK_200 }),
+          makeRawRequest({ url: file.torrentDownloadUrl, token, expectedStatus: HttpStatusCode.OK_200 }),
+          makeRawRequest({ url: file.metadataUrl, token, expectedStatus: HttpStatusCode.OK_200 }),
+          makeRawRequest({ url: file.fileUrl, token, expectedStatus: HttpStatusCode.OK_200 }),
+
+          makeRawRequest({
+            url: file.fileDownloadUrl,
+            token,
+            expectedStatus: objectStorageBaseUrl
+              ? HttpStatusCode.FOUND_302
+              : HttpStatusCode.OK_200
+          })
+        ])
+      }
     }
 
     // Check master playlist
     {
-      await checkResolutionsInMasterPlaylist({ server, playlistUrl: hlsPlaylist.playlistUrl, resolutions })
+      await checkResolutionsInMasterPlaylist({ server, token, playlistUrl: hlsPlaylist.playlistUrl, resolutions })
 
-      const masterPlaylist = await server.streamingPlaylists.get({ url: hlsPlaylist.playlistUrl })
+      const masterPlaylist = await server.streamingPlaylists.get({ url: hlsPlaylist.playlistUrl, token })
 
       let i = 0
       for (const resolution of resolutions) {
@@ -163,11 +200,16 @@ async function completeCheckHlsPlaylist (options: {
         const file = hlsFiles.find(f => f.resolution.id === resolution)
         const playlistName = removeFragmentedMP4Ext(basename(file.fileUrl)) + '.m3u8'
 
-        const url = objectStorageBaseUrl
-          ? `${objectStorageBaseUrl}hls/${videoUUID}/${playlistName}`
-          : `${baseUrl}/static/streaming-playlists/hls/${videoUUID}/${playlistName}`
+        let url: string
+        if (objectStorageBaseUrl && requiresAuth) {
+          url = `${baseUrl}/object-storage-proxy/streaming-playlists/hls/${privatePath}${videoUUID}/${playlistName}`
+        } else if (objectStorageBaseUrl) {
+          url = `${objectStorageBaseUrl}hls/${videoUUID}/${playlistName}`
+        } else {
+          url = `${baseUrl}/static/streaming-playlists/hls/${privatePath}${videoUUID}/${playlistName}`
+        }
 
-        const subPlaylist = await server.streamingPlaylists.get({ url })
+        const subPlaylist = await server.streamingPlaylists.get({ url, token })
 
         expect(subPlaylist).to.match(new RegExp(`${uuidRegex}-${resolution}-fragmented.mp4`))
         expect(subPlaylist).to.contain(basename(file.fileUrl))
@@ -175,13 +217,19 @@ async function completeCheckHlsPlaylist (options: {
     }
 
     {
-      const baseUrlAndPath = objectStorageBaseUrl
-        ? objectStorageBaseUrl + 'hls/' + videoUUID
-        : baseUrl + '/static/streaming-playlists/hls/' + videoUUID
+      let baseUrlAndPath: string
+      if (objectStorageBaseUrl && requiresAuth) {
+        baseUrlAndPath = `${baseUrl}/object-storage-proxy/streaming-playlists/hls/${privatePath}${videoUUID}`
+      } else if (objectStorageBaseUrl) {
+        baseUrlAndPath = `${objectStorageBaseUrl}hls/${videoUUID}`
+      } else {
+        baseUrlAndPath = `${baseUrl}/static/streaming-playlists/hls/${privatePath}${videoUUID}`
+      }
 
       for (const resolution of resolutions) {
         await checkSegmentHash({
           server,
+          token,
           baseUrlPlaylist: baseUrlAndPath,
           baseUrlSegment: baseUrlAndPath,
           resolution,
index f8ec65752b81530b454d7c636c52f5c61054cb44..856fabd11bee15e16db4967f3a0d7e62eb8225e3 100644 (file)
@@ -4,16 +4,106 @@ import { expect } from 'chai'
 import { pathExists, readdir } from 'fs-extra'
 import { basename, join } from 'path'
 import { loadLanguages, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '@server/initializers/constants'
-import { getLowercaseExtension, uuidRegex } from '@shared/core-utils'
-import { HttpStatusCode, VideoCaption, VideoDetails } from '@shared/models'
-import { makeRawRequest, PeerTubeServer, VideoEdit, waitJobs, webtorrentAdd } from '@shared/server-commands'
-import { dateIsValid, testImage } from './checks'
+import { getLowercaseExtension, pick, uuidRegex } from '@shared/core-utils'
+import { HttpStatusCode, VideoCaption, VideoDetails, VideoPrivacy, VideoResolution } from '@shared/models'
+import { makeRawRequest, PeerTubeServer, VideoEdit, waitJobs } from '@shared/server-commands'
+import { dateIsValid, expectStartWith, testImage } from './checks'
+import { checkWebTorrentWorks } from './webtorrent'
 
 loadLanguages()
 
-async function completeVideoCheck (
-  server: PeerTubeServer,
-  video: any,
+async function completeWebVideoFilesCheck (options: {
+  server: PeerTubeServer
+  originServer: PeerTubeServer
+  videoUUID: string
+  fixture: string
+  files: {
+    resolution: number
+    size?: number
+  }[]
+  objectStorageBaseUrl?: string
+}) {
+  const { originServer, server, videoUUID, files, fixture, objectStorageBaseUrl } = options
+  const video = await server.videos.getWithToken({ id: videoUUID })
+  const serverConfig = await originServer.config.getConfig()
+  const requiresAuth = video.privacy.id === VideoPrivacy.PRIVATE || video.privacy.id === VideoPrivacy.INTERNAL
+
+  const transcodingEnabled = serverConfig.transcoding.webtorrent.enabled
+
+  for (const attributeFile of files) {
+    const file = video.files.find(f => f.resolution.id === attributeFile.resolution)
+    expect(file, `resolution ${attributeFile.resolution} does not exist`).not.to.be.undefined
+
+    let extension = getLowercaseExtension(fixture)
+    // Transcoding enabled: extension will always be .mp4
+    if (transcodingEnabled) extension = '.mp4'
+
+    expect(file.id).to.exist
+    expect(file.magnetUri).to.have.lengthOf.above(2)
+
+    {
+      const privatePath = requiresAuth
+        ? 'private/'
+        : ''
+      const nameReg = `${uuidRegex}-${file.resolution.id}`
+
+      expect(file.torrentDownloadUrl).to.match(new RegExp(`${server.url}/download/torrents/${nameReg}.torrent`))
+      expect(file.torrentUrl).to.match(new RegExp(`${server.url}/lazy-static/torrents/${nameReg}.torrent`))
+
+      if (objectStorageBaseUrl && requiresAuth) {
+        expect(file.fileUrl).to.match(new RegExp(`${originServer.url}/object-storage-proxy/webseed/${privatePath}${nameReg}${extension}`))
+      } else if (objectStorageBaseUrl) {
+        expectStartWith(file.fileUrl, objectStorageBaseUrl)
+      } else {
+        expect(file.fileUrl).to.match(new RegExp(`${originServer.url}/static/webseed/${privatePath}${nameReg}${extension}`))
+      }
+
+      expect(file.fileDownloadUrl).to.match(new RegExp(`${originServer.url}/download/videos/${nameReg}${extension}`))
+    }
+
+    {
+      const token = requiresAuth
+        ? server.accessToken
+        : undefined
+
+      await Promise.all([
+        makeRawRequest({ url: file.torrentUrl, token, expectedStatus: HttpStatusCode.OK_200 }),
+        makeRawRequest({ url: file.torrentDownloadUrl, token, expectedStatus: HttpStatusCode.OK_200 }),
+        makeRawRequest({ url: file.metadataUrl, token, expectedStatus: HttpStatusCode.OK_200 }),
+        makeRawRequest({ url: file.fileUrl, token, expectedStatus: HttpStatusCode.OK_200 }),
+        makeRawRequest({
+          url: file.fileDownloadUrl,
+          token,
+          expectedStatus: objectStorageBaseUrl ? HttpStatusCode.FOUND_302 : HttpStatusCode.OK_200
+        })
+      ])
+    }
+
+    expect(file.resolution.id).to.equal(attributeFile.resolution)
+
+    if (file.resolution.id === VideoResolution.H_NOVIDEO) {
+      expect(file.resolution.label).to.equal('Audio')
+    } else {
+      expect(file.resolution.label).to.equal(attributeFile.resolution + 'p')
+    }
+
+    if (attributeFile.size) {
+      const minSize = attributeFile.size - ((10 * attributeFile.size) / 100)
+      const maxSize = attributeFile.size + ((10 * attributeFile.size) / 100)
+      expect(
+        file.size,
+        'File size for resolution ' + file.resolution.label + ' outside confidence interval (' + minSize + '> size <' + maxSize + ')'
+      ).to.be.above(minSize).and.below(maxSize)
+    }
+
+    await checkWebTorrentWorks(file.magnetUri)
+  }
+}
+
+async function completeVideoCheck (options: {
+  server: PeerTubeServer
+  originServer: PeerTubeServer
+  videoUUID: string
   attributes: {
     name: string
     category: number
@@ -50,13 +140,14 @@ async function completeVideoCheck (
     thumbnailfile?: string
     previewfile?: string
   }
-) {
+}) {
+  const { attributes, originServer, server, videoUUID } = options
+
+  const video = await server.videos.get({ id: videoUUID })
+
   if (!attributes.likes) attributes.likes = 0
   if (!attributes.dislikes) attributes.dislikes = 0
 
-  const host = new URL(server.url).host
-  const originHost = attributes.account.host
-
   expect(video.name).to.equal(attributes.name)
   expect(video.category.id).to.equal(attributes.category)
   expect(video.category.label).to.equal(attributes.category !== null ? VIDEO_CATEGORIES[attributes.category] : 'Unknown')
@@ -77,7 +168,7 @@ async function completeVideoCheck (
   expect(video.dislikes).to.equal(attributes.dislikes)
   expect(video.isLocal).to.equal(attributes.isLocal)
   expect(video.duration).to.equal(attributes.duration)
-  expect(video.url).to.contain(originHost)
+  expect(video.url).to.contain(originServer.host)
   expect(dateIsValid(video.createdAt)).to.be.true
   expect(dateIsValid(video.publishedAt)).to.be.true
   expect(dateIsValid(video.updatedAt)).to.be.true
@@ -92,67 +183,28 @@ async function completeVideoCheck (
     expect(video.originallyPublishedAt).to.be.null
   }
 
-  const videoDetails = await server.videos.get({ id: video.uuid })
-
-  expect(videoDetails.files).to.have.lengthOf(attributes.files.length)
-  expect(videoDetails.tags).to.deep.equal(attributes.tags)
-  expect(videoDetails.account.name).to.equal(attributes.account.name)
-  expect(videoDetails.account.host).to.equal(attributes.account.host)
+  expect(video.files).to.have.lengthOf(attributes.files.length)
+  expect(video.tags).to.deep.equal(attributes.tags)
+  expect(video.account.name).to.equal(attributes.account.name)
+  expect(video.account.host).to.equal(attributes.account.host)
   expect(video.channel.displayName).to.equal(attributes.channel.displayName)
   expect(video.channel.name).to.equal(attributes.channel.name)
-  expect(videoDetails.channel.host).to.equal(attributes.account.host)
-  expect(videoDetails.channel.isLocal).to.equal(attributes.channel.isLocal)
-  expect(dateIsValid(videoDetails.channel.createdAt.toString())).to.be.true
-  expect(dateIsValid(videoDetails.channel.updatedAt.toString())).to.be.true
-  expect(videoDetails.commentsEnabled).to.equal(attributes.commentsEnabled)
-  expect(videoDetails.downloadEnabled).to.equal(attributes.downloadEnabled)
-
-  for (const attributeFile of attributes.files) {
-    const file = videoDetails.files.find(f => f.resolution.id === attributeFile.resolution)
-    expect(file).not.to.be.undefined
-
-    let extension = getLowercaseExtension(attributes.fixture)
-    // Transcoding enabled: extension will always be .mp4
-    if (attributes.files.length > 1) extension = '.mp4'
-
-    expect(file.id).to.exist
-    expect(file.magnetUri).to.have.lengthOf.above(2)
-
-    expect(file.torrentDownloadUrl).to.match(new RegExp(`http://${host}/download/torrents/${uuidRegex}-${file.resolution.id}.torrent`))
-    expect(file.torrentUrl).to.match(new RegExp(`http://${host}/lazy-static/torrents/${uuidRegex}-${file.resolution.id}.torrent`))
-
-    expect(file.fileUrl).to.match(new RegExp(`http://${originHost}/static/webseed/${uuidRegex}-${file.resolution.id}${extension}`))
-    expect(file.fileDownloadUrl).to.match(new RegExp(`http://${originHost}/download/videos/${uuidRegex}-${file.resolution.id}${extension}`))
+  expect(video.channel.host).to.equal(attributes.account.host)
+  expect(video.channel.isLocal).to.equal(attributes.channel.isLocal)
+  expect(dateIsValid(video.channel.createdAt.toString())).to.be.true
+  expect(dateIsValid(video.channel.updatedAt.toString())).to.be.true
+  expect(video.commentsEnabled).to.equal(attributes.commentsEnabled)
+  expect(video.downloadEnabled).to.equal(attributes.downloadEnabled)
 
-    await Promise.all([
-      makeRawRequest({ url: file.torrentUrl, expectedStatus: HttpStatusCode.OK_200 }),
-      makeRawRequest({ url: file.torrentDownloadUrl, expectedStatus: HttpStatusCode.OK_200 }),
-      makeRawRequest({ url: file.metadataUrl, expectedStatus: HttpStatusCode.OK_200 })
-    ])
-
-    expect(file.resolution.id).to.equal(attributeFile.resolution)
-    expect(file.resolution.label).to.equal(attributeFile.resolution + 'p')
-
-    const minSize = attributeFile.size - ((10 * attributeFile.size) / 100)
-    const maxSize = attributeFile.size + ((10 * attributeFile.size) / 100)
-    expect(
-      file.size,
-      'File size for resolution ' + file.resolution.label + ' outside confidence interval (' + minSize + '> size <' + maxSize + ')'
-    ).to.be.above(minSize).and.below(maxSize)
-
-    const torrent = await webtorrentAdd(file.magnetUri, true)
-    expect(torrent.files).to.be.an('array')
-    expect(torrent.files.length).to.equal(1)
-    expect(torrent.files[0].path).to.exist.and.to.not.equal('')
-  }
-
-  expect(videoDetails.thumbnailPath).to.exist
-  await testImage(server.url, attributes.thumbnailfile || attributes.fixture, videoDetails.thumbnailPath)
+  expect(video.thumbnailPath).to.exist
+  await testImage(server.url, attributes.thumbnailfile || attributes.fixture, video.thumbnailPath)
 
   if (attributes.previewfile) {
-    expect(videoDetails.previewPath).to.exist
-    await testImage(server.url, attributes.previewfile, videoDetails.previewPath)
+    expect(video.previewPath).to.exist
+    await testImage(server.url, attributes.previewfile, video.previewPath)
   }
+
+  await completeWebVideoFilesCheck({ server, originServer, videoUUID: video.uuid, ...pick(attributes, [ 'fixture', 'files' ]) })
 }
 
 async function checkVideoFilesWereRemoved (options: {
@@ -245,6 +297,7 @@ async function uploadRandomVideoOnServers (
 
 export {
   completeVideoCheck,
+  completeWebVideoFilesCheck,
   checkUploadVideoParam,
   uploadRandomVideoOnServers,
   checkVideoFilesWereRemoved,
similarity index 62%
rename from shared/server-commands/miscs/webtorrent.ts
rename to server/tests/shared/webtorrent.ts
index 0683f889388170651036d884d6112abca41ebf86..d5bd86500dd8dec3f23409a22bbe74b8659bde12 100644 (file)
@@ -1,12 +1,38 @@
+import { expect } from 'chai'
 import { readFile } from 'fs-extra'
 import parseTorrent from 'parse-torrent'
 import { basename, join } from 'path'
 import * as WebTorrent from 'webtorrent'
 import { VideoFile } from '@shared/models'
-import { PeerTubeServer } from '../server'
+import { PeerTubeServer } from '@shared/server-commands'
 
 let webtorrent: WebTorrent.Instance
 
+export async function checkWebTorrentWorks (magnetUri: string, pathMatch?: RegExp) {
+  const torrent = await webtorrentAdd(magnetUri, true)
+
+  expect(torrent.files).to.be.an('array')
+  expect(torrent.files.length).to.equal(1)
+  expect(torrent.files[0].path).to.exist.and.to.not.equal('')
+
+  if (pathMatch) {
+    expect(torrent.files[0].path).match(pathMatch)
+  }
+}
+
+export async function parseTorrentVideo (server: PeerTubeServer, file: VideoFile) {
+  const torrentName = basename(file.torrentUrl)
+  const torrentPath = server.servers.buildDirectory(join('torrents', torrentName))
+
+  const data = await readFile(torrentPath)
+
+  return parseTorrent(data)
+}
+
+// ---------------------------------------------------------------------------
+// Private
+// ---------------------------------------------------------------------------
+
 function webtorrentAdd (torrentId: string, refreshWebTorrent = false) {
   const WebTorrent = require('webtorrent')
 
@@ -30,17 +56,3 @@ function webtorrentAdd (torrentId: string, refreshWebTorrent = false) {
     })
   })
 }
-
-async function parseTorrentVideo (server: PeerTubeServer, file: VideoFile) {
-  const torrentName = basename(file.torrentUrl)
-  const torrentPath = server.servers.buildDirectory(join('torrents', torrentName))
-
-  const data = await readFile(torrentPath)
-
-  return parseTorrent(data)
-}
-
-export {
-  webtorrentAdd,
-  parseTorrentVideo
-}
index 4be7ae2f49be78763fc21d3154824c8e22ed4299..240bd3bfedeff56ee5e641651e27a938de04e2e5 100644 (file)
@@ -7,6 +7,7 @@
     { "path": "../shared" }
   ],
   "exclude": [
-    "tools/"
+    "tools/",
+    "tests/fixtures"
   ]
 }
index c24ebb2df295a4c53de538ca61980d483b23b962..a4581dbc0b44ebf1234a15b2570cf8333fa4e0b0 100644 (file)
@@ -3,10 +3,10 @@ export * from './cli'
 export * from './custom-pages'
 export * from './feeds'
 export * from './logs'
-export * from './miscs'
 export * from './moderation'
 export * from './overviews'
 export * from './requests'
+export * from './runners'
 export * from './search'
 export * from './server'
 export * from './socket'
diff --git a/shared/server-commands/miscs/index.ts b/shared/server-commands/miscs/index.ts
deleted file mode 100644 (file)
index a1d14e9..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-export * from './sql-command'
-export * from './webtorrent'
index cb0e1a5fbd920f3646f9f4c9d1a3b3022977e4b4..96f67b4c7fbc1e0973c0b3cc8806619491c6cd98 100644 (file)
@@ -10,6 +10,7 @@ export type CommonRequestParams = {
   url: string
   path?: string
   contentType?: string
+  responseType?: string
   range?: string
   redirects?: number
   accept?: string
@@ -27,16 +28,23 @@ function makeRawRequest (options: {
   expectedStatus?: HttpStatusCode
   range?: string
   query?: { [ id: string ]: string }
+  method?: 'GET' | 'POST'
 }) {
   const { host, protocol, pathname } = new URL(options.url)
 
-  return makeGetRequest({
+  const reqOptions = {
     url: `${protocol}//${host}`,
     path: pathname,
     contentType: undefined,
 
     ...pick(options, [ 'expectedStatus', 'range', 'token', 'query' ])
-  })
+  }
+
+  if (options.method === 'POST') {
+    return makePostBodyRequest(reqOptions)
+  }
+
+  return makeGetRequest(reqOptions)
 }
 
 function makeGetRequest (options: CommonRequestParams & {
@@ -135,6 +143,8 @@ function decodeQueryString (path: string) {
   return decode(path.split('?')[1])
 }
 
+// ---------------------------------------------------------------------------
+
 function unwrapBody <T> (test: request.Test): Promise<T> {
   return test.then(res => res.body)
 }
@@ -149,7 +159,16 @@ function unwrapBodyOrDecodeToJSON <T> (test: request.Test): Promise<T> {
       try {
         return JSON.parse(new TextDecoder().decode(res.body))
       } catch (err) {
-        console.error('Cannot decode JSON.', res.body)
+        console.error('Cannot decode JSON.', res.body instanceof Buffer ? res.body.toString() : res.body)
+        throw err
+      }
+    }
+
+    if (res.text) {
+      try {
+        return JSON.parse(res.text)
+      } catch (err) {
+        console.error('Cannot decode json', res.text)
         throw err
       }
     }
@@ -184,6 +203,7 @@ export {
 
 function buildRequest (req: request.Test, options: CommonRequestParams) {
   if (options.contentType) req.set('Accept', options.contentType)
+  if (options.responseType) req.responseType(options.responseType)
   if (options.token) req.set('Authorization', 'Bearer ' + options.token)
   if (options.range) req.set('Range', options.range)
   if (options.accept) req.set('Accept', options.accept)
@@ -196,13 +216,18 @@ function buildRequest (req: request.Test, options: CommonRequestParams) {
     req.set(name, options.headers[name])
   })
 
-  return req.expect((res) => {
+  return req.expect(res => {
     if (options.expectedStatus && res.status !== options.expectedStatus) {
-      throw new Error(`Expected status ${options.expectedStatus}, got ${res.status}. ` +
+      const err = new Error(`Expected status ${options.expectedStatus}, got ${res.status}. ` +
         `\nThe server responded: "${res.body?.error ?? res.text}".\n` +
         'You may take a closer look at the logs. To see how to do so, check out this page: ' +
-        'https://github.com/Chocobozzz/PeerTube/blob/develop/support/doc/development/tests.md#debug-server-logs')
+        'https://github.com/Chocobozzz/PeerTube/blob/develop/support/doc/development/tests.md#debug-server-logs');
+
+      (err as any).res = res
+
+      throw err
     }
+
     return res
   })
 }
diff --git a/shared/server-commands/runners/index.ts b/shared/server-commands/runners/index.ts
new file mode 100644 (file)
index 0000000..9e8e1ba
--- /dev/null
@@ -0,0 +1,3 @@
+export * from './runner-jobs-command'
+export * from './runner-registration-tokens-command'
+export * from './runners-command'
diff --git a/shared/server-commands/runners/runner-jobs-command.ts b/shared/server-commands/runners/runner-jobs-command.ts
new file mode 100644 (file)
index 0000000..3b0f84b
--- /dev/null
@@ -0,0 +1,279 @@
+import { omit, pick, wait } from '@shared/core-utils'
+import {
+  AbortRunnerJobBody,
+  AcceptRunnerJobBody,
+  AcceptRunnerJobResult,
+  ErrorRunnerJobBody,
+  HttpStatusCode,
+  isHLSTranscodingPayloadSuccess,
+  isLiveRTMPHLSTranscodingUpdatePayload,
+  isWebVideoOrAudioMergeTranscodingPayloadSuccess,
+  RequestRunnerJobBody,
+  RequestRunnerJobResult,
+  ResultList,
+  RunnerJobAdmin,
+  RunnerJobLiveRTMPHLSTranscodingPayload,
+  RunnerJobPayload,
+  RunnerJobState,
+  RunnerJobSuccessBody,
+  RunnerJobSuccessPayload,
+  RunnerJobType,
+  RunnerJobUpdateBody,
+  RunnerJobVODPayload
+} from '@shared/models'
+import { unwrapBody } from '../requests'
+import { waitJobs } from '../server'
+import { AbstractCommand, OverrideCommandOptions } from '../shared'
+
+export class RunnerJobsCommand extends AbstractCommand {
+
+  list (options: OverrideCommandOptions & {
+    start?: number
+    count?: number
+    sort?: string
+    search?: string
+  } = {}) {
+    const path = '/api/v1/runners/jobs'
+
+    return this.getRequestBody<ResultList<RunnerJobAdmin>>({
+      ...options,
+
+      path,
+      query: pick(options, [ 'start', 'count', 'sort', 'search' ]),
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  cancelByAdmin (options: OverrideCommandOptions & { jobUUID: string }) {
+    const path = '/api/v1/runners/jobs/' + options.jobUUID + '/cancel'
+
+    return this.postBodyRequest({
+      ...options,
+
+      path,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+
+  // ---------------------------------------------------------------------------
+
+  request (options: OverrideCommandOptions & RequestRunnerJobBody) {
+    const path = '/api/v1/runners/jobs/request'
+
+    return unwrapBody<RequestRunnerJobResult>(this.postBodyRequest({
+      ...options,
+
+      path,
+      fields: pick(options, [ 'runnerToken' ]),
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    }))
+  }
+
+  async requestVOD (options: OverrideCommandOptions & RequestRunnerJobBody) {
+    const vodTypes = new Set<RunnerJobType>([ 'vod-audio-merge-transcoding', 'vod-hls-transcoding', 'vod-web-video-transcoding' ])
+
+    const { availableJobs } = await this.request(options)
+
+    return {
+      availableJobs: availableJobs.filter(j => vodTypes.has(j.type))
+    } as RequestRunnerJobResult<RunnerJobVODPayload>
+  }
+
+  async requestLive (options: OverrideCommandOptions & RequestRunnerJobBody) {
+    const vodTypes = new Set<RunnerJobType>([ 'live-rtmp-hls-transcoding' ])
+
+    const { availableJobs } = await this.request(options)
+
+    return {
+      availableJobs: availableJobs.filter(j => vodTypes.has(j.type))
+    } as RequestRunnerJobResult<RunnerJobLiveRTMPHLSTranscodingPayload>
+  }
+
+  // ---------------------------------------------------------------------------
+
+  accept <T extends RunnerJobPayload = RunnerJobPayload> (options: OverrideCommandOptions & AcceptRunnerJobBody & { jobUUID: string }) {
+    const path = '/api/v1/runners/jobs/' + options.jobUUID + '/accept'
+
+    return unwrapBody<AcceptRunnerJobResult<T>>(this.postBodyRequest({
+      ...options,
+
+      path,
+      fields: pick(options, [ 'runnerToken' ]),
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    }))
+  }
+
+  abort (options: OverrideCommandOptions & AbortRunnerJobBody & { jobUUID: string }) {
+    const path = '/api/v1/runners/jobs/' + options.jobUUID + '/abort'
+
+    return this.postBodyRequest({
+      ...options,
+
+      path,
+      fields: pick(options, [ 'reason', 'jobToken', 'runnerToken' ]),
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+
+  update (options: OverrideCommandOptions & RunnerJobUpdateBody & { jobUUID: string }) {
+    const path = '/api/v1/runners/jobs/' + options.jobUUID + '/update'
+
+    const { payload } = options
+    const attaches: { [id: string]: any } = {}
+    let payloadWithoutFiles = payload
+
+    if (isLiveRTMPHLSTranscodingUpdatePayload(payload)) {
+      if (payload.masterPlaylistFile) {
+        attaches[`payload[masterPlaylistFile]`] = payload.masterPlaylistFile
+      }
+
+      attaches[`payload[resolutionPlaylistFile]`] = payload.resolutionPlaylistFile
+      attaches[`payload[videoChunkFile]`] = payload.videoChunkFile
+
+      payloadWithoutFiles = omit(payloadWithoutFiles as any, [ 'masterPlaylistFile', 'resolutionPlaylistFile', 'videoChunkFile' ])
+    }
+
+    return this.postUploadRequest({
+      ...options,
+
+      path,
+      fields: {
+        ...pick(options, [ 'progress', 'jobToken', 'runnerToken' ]),
+
+        payload: payloadWithoutFiles
+      },
+      attaches,
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+
+  error (options: OverrideCommandOptions & ErrorRunnerJobBody & { jobUUID: string }) {
+    const path = '/api/v1/runners/jobs/' + options.jobUUID + '/error'
+
+    return this.postBodyRequest({
+      ...options,
+
+      path,
+      fields: pick(options, [ 'message', 'jobToken', 'runnerToken' ]),
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+
+  success (options: OverrideCommandOptions & RunnerJobSuccessBody & { jobUUID: string }) {
+    const { payload } = options
+
+    const path = '/api/v1/runners/jobs/' + options.jobUUID + '/success'
+    const attaches: { [id: string]: any } = {}
+    let payloadWithoutFiles = payload
+
+    if ((isWebVideoOrAudioMergeTranscodingPayloadSuccess(payload) || isHLSTranscodingPayloadSuccess(payload)) && payload.videoFile) {
+      attaches[`payload[videoFile]`] = payload.videoFile
+
+      payloadWithoutFiles = omit(payloadWithoutFiles as any, [ 'videoFile' ])
+    }
+
+    if (isHLSTranscodingPayloadSuccess(payload) && payload.resolutionPlaylistFile) {
+      attaches[`payload[resolutionPlaylistFile]`] = payload.resolutionPlaylistFile
+
+      payloadWithoutFiles = omit(payloadWithoutFiles as any, [ 'resolutionPlaylistFile' ])
+    }
+
+    return this.postUploadRequest({
+      ...options,
+
+      path,
+      attaches,
+      fields: {
+        ...pick(options, [ 'jobToken', 'runnerToken' ]),
+
+        payload: payloadWithoutFiles
+      },
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+
+  getInputFile (options: OverrideCommandOptions & { url: string, jobToken: string, runnerToken: string }) {
+    const { host, protocol, pathname } = new URL(options.url)
+
+    return this.postBodyRequest({
+      url: `${protocol}//${host}`,
+      path: pathname,
+
+      fields: pick(options, [ 'jobToken', 'runnerToken' ]),
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  // ---------------------------------------------------------------------------
+
+  async autoAccept (options: OverrideCommandOptions & RequestRunnerJobBody & { type?: RunnerJobType }) {
+    const { availableJobs } = await this.request(options)
+
+    const job = options.type
+      ? availableJobs.find(j => j.type === options.type)
+      : availableJobs[0]
+
+    return this.accept({ ...options, jobUUID: job.uuid })
+  }
+
+  async autoProcessWebVideoJob (runnerToken: string, jobUUIDToProcess?: string) {
+    let jobUUID = jobUUIDToProcess
+
+    if (!jobUUID) {
+      const { availableJobs } = await this.request({ runnerToken })
+      jobUUID = availableJobs[0].uuid
+    }
+
+    const { job } = await this.accept({ runnerToken, jobUUID })
+    const jobToken = job.jobToken
+
+    const payload: RunnerJobSuccessPayload = { videoFile: 'video_short.mp4' }
+    await this.success({ runnerToken, jobUUID, jobToken, payload })
+
+    await waitJobs([ this.server ])
+
+    return job
+  }
+
+  async cancelAllJobs (options: { state?: RunnerJobState } = {}) {
+    const { state } = options
+
+    const { data } = await this.list({ count: 100 })
+
+    for (const job of data) {
+      if (state && job.state.id !== state) continue
+
+      await this.cancelByAdmin({ jobUUID: job.uuid })
+    }
+  }
+
+  async getJob (options: OverrideCommandOptions & { uuid: string }) {
+    const { data } = await this.list({ ...options, count: 100, sort: '-updatedAt' })
+
+    return data.find(j => j.uuid === options.uuid)
+  }
+
+  async requestLiveJob (runnerToken: string) {
+    let availableJobs: RequestRunnerJobResult<RunnerJobLiveRTMPHLSTranscodingPayload>['availableJobs'] = []
+
+    while (availableJobs.length === 0) {
+      const result = await this.requestLive({ runnerToken })
+      availableJobs = result.availableJobs
+
+      if (availableJobs.length === 1) break
+
+      await wait(150)
+    }
+
+    return availableJobs[0]
+  }
+}
diff --git a/shared/server-commands/runners/runner-registration-tokens-command.ts b/shared/server-commands/runners/runner-registration-tokens-command.ts
new file mode 100644 (file)
index 0000000..e4f2e3d
--- /dev/null
@@ -0,0 +1,55 @@
+import { pick } from '@shared/core-utils'
+import { HttpStatusCode, ResultList, RunnerRegistrationToken } from '@shared/models'
+import { AbstractCommand, OverrideCommandOptions } from '../shared'
+
+export class RunnerRegistrationTokensCommand extends AbstractCommand {
+
+  list (options: OverrideCommandOptions & {
+    start?: number
+    count?: number
+    sort?: string
+  } = {}) {
+    const path = '/api/v1/runners/registration-tokens'
+
+    return this.getRequestBody<ResultList<RunnerRegistrationToken>>({
+      ...options,
+
+      path,
+      query: pick(options, [ 'start', 'count', 'sort' ]),
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  generate (options: OverrideCommandOptions = {}) {
+    const path = '/api/v1/runners/registration-tokens/generate'
+
+    return this.postBodyRequest({
+      ...options,
+
+      path,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+
+  delete (options: OverrideCommandOptions & {
+    id: number
+  }) {
+    const path = '/api/v1/runners/registration-tokens/' + options.id
+
+    return this.deleteRequest({
+      ...options,
+
+      path,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+
+  async getFirstRegistrationToken (options: OverrideCommandOptions = {}) {
+    const { data } = await this.list(options)
+
+    return data[0].registrationToken
+  }
+}
diff --git a/shared/server-commands/runners/runners-command.ts b/shared/server-commands/runners/runners-command.ts
new file mode 100644 (file)
index 0000000..ca9a1d7
--- /dev/null
@@ -0,0 +1,77 @@
+import { pick } from '@shared/core-utils'
+import { HttpStatusCode, RegisterRunnerBody, RegisterRunnerResult, ResultList, Runner, UnregisterRunnerBody } from '@shared/models'
+import { unwrapBody } from '../requests'
+import { AbstractCommand, OverrideCommandOptions } from '../shared'
+
+export class RunnersCommand extends AbstractCommand {
+
+  list (options: OverrideCommandOptions & {
+    start?: number
+    count?: number
+    sort?: string
+  } = {}) {
+    const path = '/api/v1/runners'
+
+    return this.getRequestBody<ResultList<Runner>>({
+      ...options,
+
+      path,
+      query: pick(options, [ 'start', 'count', 'sort' ]),
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  register (options: OverrideCommandOptions & RegisterRunnerBody) {
+    const path = '/api/v1/runners/register'
+
+    return unwrapBody<RegisterRunnerResult>(this.postBodyRequest({
+      ...options,
+
+      path,
+      fields: pick(options, [ 'name', 'registrationToken', 'description' ]),
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    }))
+  }
+
+  unregister (options: OverrideCommandOptions & UnregisterRunnerBody) {
+    const path = '/api/v1/runners/unregister'
+
+    return this.postBodyRequest({
+      ...options,
+
+      path,
+      fields: pick(options, [ 'runnerToken' ]),
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+
+  delete (options: OverrideCommandOptions & {
+    id: number
+  }) {
+    const path = '/api/v1/runners/' + options.id
+
+    return this.deleteRequest({
+      ...options,
+
+      path,
+      implicitToken: true,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+
+  // ---------------------------------------------------------------------------
+
+  async autoRegisterRunner () {
+    const { data } = await this.server.runnerRegistrationTokens.list({ sort: 'createdAt' })
+
+    const { runnerToken } = await this.register({
+      name: 'runner',
+      registrationToken: data[0].registrationToken
+    })
+
+    return runnerToken
+  }
+}
index 303fcab88adc02e8d1fc85877791a9e0410cd90c..9a6e413f2b4ecdcd70a23c1bf34850aa7ec3519a 100644 (file)
@@ -5,8 +5,9 @@ import { AbstractCommand, OverrideCommandOptions } from '../shared/abstract-comm
 
 export class ConfigCommand extends AbstractCommand {
 
-  static getCustomConfigResolutions (enabled: boolean) {
+  static getCustomConfigResolutions (enabled: boolean, with0p = false) {
     return {
+      '0p': enabled && with0p,
       '144p': enabled,
       '240p': enabled,
       '360p': enabled,
@@ -129,7 +130,8 @@ export class ConfigCommand extends AbstractCommand {
     })
   }
 
-  enableTranscoding (webtorrent = true, hls = true) {
+  // TODO: convert args to object
+  enableTranscoding (webtorrent = true, hls = true, with0p = false) {
     return this.updateExistingSubConfig({
       newConfig: {
         transcoding: {
@@ -138,7 +140,7 @@ export class ConfigCommand extends AbstractCommand {
           allowAudioFiles: true,
           allowAdditionalExtensions: true,
 
-          resolutions: ConfigCommand.getCustomConfigResolutions(true),
+          resolutions: ConfigCommand.getCustomConfigResolutions(true, with0p),
 
           webtorrent: {
             enabled: webtorrent
@@ -151,6 +153,7 @@ export class ConfigCommand extends AbstractCommand {
     })
   }
 
+  // TODO: convert args to object
   enableMinimumTranscoding (webtorrent = true, hls = true) {
     return this.updateExistingSubConfig({
       newConfig: {
@@ -173,6 +176,25 @@ export class ConfigCommand extends AbstractCommand {
     })
   }
 
+  enableRemoteTranscoding () {
+    return this.updateExistingSubConfig({
+      newConfig: {
+        transcoding: {
+          remoteRunners: {
+            enabled: true
+          }
+        },
+        live: {
+          transcoding: {
+            remoteRunners: {
+              enabled: true
+            }
+          }
+        }
+      }
+    })
+  }
+
   // ---------------------------------------------------------------------------
 
   enableStudio () {
@@ -363,6 +385,9 @@ export class ConfigCommand extends AbstractCommand {
       },
       transcoding: {
         enabled: true,
+        remoteRunners: {
+          enabled: false
+        },
         allowAdditionalExtensions: true,
         allowAudioFiles: true,
         threads: 1,
@@ -398,6 +423,9 @@ export class ConfigCommand extends AbstractCommand {
         maxUserLives: 50,
         transcoding: {
           enabled: true,
+          remoteRunners: {
+            enabled: false
+          },
           threads: 4,
           profile: 'default',
           resolutions: {
index e1d6cdff4034f031cc58d03db18cba7277c3c0e6..ff309806351c9e088b305236b447246bc6818f11 100644 (file)
@@ -1,16 +1,17 @@
 
 import { expect } from 'chai'
 import { wait } from '@shared/core-utils'
-import { JobState, JobType } from '../../models'
+import { JobState, JobType, RunnerJobState } from '../../models'
 import { PeerTubeServer } from './server'
 
 async function waitJobs (
   serversArg: PeerTubeServer[] | PeerTubeServer,
   options: {
     skipDelayed?: boolean // default false
+    runnerJobs?: boolean // default false
   } = {}
 ) {
-  const { skipDelayed = false } = options
+  const { skipDelayed = false, runnerJobs = false } = options
 
   const pendingJobWait = process.env.NODE_PENDING_JOB_WAIT
     ? parseInt(process.env.NODE_PENDING_JOB_WAIT, 10)
@@ -33,7 +34,8 @@ async function waitJobs (
     // Check if each server has pending request
     for (const server of servers) {
       for (const state of states) {
-        const p = server.jobs.list({
+
+        const jobPromise = server.jobs.list({
           state,
           start: 0,
           count: 10,
@@ -46,17 +48,29 @@ async function waitJobs (
             }
           })
 
-        tasks.push(p)
+        tasks.push(jobPromise)
       }
 
-      const p = server.debug.getDebug()
+      const debugPromise = server.debug.getDebug()
         .then(obj => {
           if (obj.activityPubMessagesWaiting !== 0) {
             pendingRequests = true
           }
         })
+      tasks.push(debugPromise)
+
+      if (runnerJobs) {
+        const runnerJobsPromise = server.runnerJobs.list({ count: 100 })
+          .then(({ data }) => {
+            for (const job of data) {
+              if (job.state.id !== RunnerJobState.COMPLETED) {
+                pendingRequests = true
+              }
+            }
+          })
+        tasks.push(runnerJobsPromise)
+      }
 
-      tasks.push(p)
     }
 
     return tasks
index d7e7515819f2fb58b72a54abcaddff74a76b0d3a..f68b8136776e3af967a8c67232f0b427b5c2d978 100644 (file)
@@ -8,9 +8,9 @@ import { CLICommand } from '../cli'
 import { CustomPagesCommand } from '../custom-pages'
 import { FeedCommand } from '../feeds'
 import { LogsCommand } from '../logs'
-import { SQLCommand } from '../miscs'
 import { AbusesCommand } from '../moderation'
 import { OverviewsCommand } from '../overviews'
+import { RunnerJobsCommand, RunnerRegistrationTokensCommand, RunnersCommand } from '../runners'
 import { SearchCommand } from '../search'
 import { SocketIOCommand } from '../socket'
 import {
@@ -136,7 +136,6 @@ export class PeerTubeServer {
   streamingPlaylists?: StreamingPlaylistsCommand
   channels?: ChannelsCommand
   comments?: CommentsCommand
-  sql?: SQLCommand
   notifications?: NotificationsCommand
   servers?: ServersCommand
   login?: LoginCommand
@@ -150,6 +149,10 @@ export class PeerTubeServer {
   videoToken?: VideoTokenCommand
   registrations?: RegistrationsCommand
 
+  runners?: RunnersCommand
+  runnerRegistrationTokens?: RunnerRegistrationTokensCommand
+  runnerJobs?: RunnerJobsCommand
+
   constructor (options: { serverNumber: number } | { url: string }) {
     if ((options as any).url) {
       this.setUrl((options as any).url)
@@ -311,14 +314,14 @@ export class PeerTubeServer {
     })
   }
 
-  async kill () {
-    if (!this.app) return
-
-    await this.sql.cleanup()
+  kill () {
+    if (!this.app) return Promise.resolve()
 
     process.kill(-this.app.pid)
 
     this.app = null
+
+    return Promise.resolve()
   }
 
   private randomServer () {
@@ -420,7 +423,6 @@ export class PeerTubeServer {
     this.streamingPlaylists = new StreamingPlaylistsCommand(this)
     this.channels = new ChannelsCommand(this)
     this.comments = new CommentsCommand(this)
-    this.sql = new SQLCommand(this)
     this.notifications = new NotificationsCommand(this)
     this.servers = new ServersCommand(this)
     this.login = new LoginCommand(this)
@@ -433,5 +435,9 @@ export class PeerTubeServer {
     this.twoFactor = new TwoFactorCommand(this)
     this.videoToken = new VideoTokenCommand(this)
     this.registrations = new RegistrationsCommand(this)
+
+    this.runners = new RunnersCommand(this)
+    this.runnerRegistrationTokens = new RunnerRegistrationTokensCommand(this)
+    this.runnerJobs = new RunnerJobsCommand(this)
   }
 }
index b2b61adb39c6c5348360f02acc4fd68a67a1ea54..fe9da9e63de934ae970a7bc4f912c3a08d3a1ff4 100644 (file)
@@ -20,7 +20,7 @@ function createMultipleServers (totalServers: number, configOverride?: object, o
   return Promise.all(serverPromises)
 }
 
-async function killallServers (servers: PeerTubeServer[]) {
+function killallServers (servers: PeerTubeServer[]) {
   return Promise.all(servers.map(s => s.kill()))
 }
 
index 1b53a53305d89cbc389adf655d288ad1287340e5..ca4ffada91a0d5f3635dc2822a4801df52643d5e 100644 (file)
@@ -33,6 +33,7 @@ interface InternalCommonCommandOptions extends OverrideCommandOptions {
   host?: string
   headers?: { [ name: string ]: string }
   requestType?: string
+  responseType?: string
   xForwardedFor?: string
 }
 
@@ -169,7 +170,7 @@ abstract class AbstractCommand {
   }
 
   protected buildCommonRequestOptions (options: InternalCommonCommandOptions) {
-    const { url, path, redirects, contentType, accept, range, host, headers, requestType, xForwardedFor } = options
+    const { url, path, redirects, contentType, accept, range, host, headers, requestType, xForwardedFor, responseType } = options
 
     return {
       url: url ?? this.server.url,
@@ -185,6 +186,7 @@ abstract class AbstractCommand {
       accept,
       headers,
       type: requestType,
+      responseType,
       xForwardedFor
     }
   }
index c277ead28151a8300df5aabaee793c99b1f6c628..c28a86366ff9c03de1f8b0cc54b8ea9a12d82efb 100644 (file)
@@ -12,4 +12,13 @@ export class SocketIOCommand extends AbstractCommand {
   getLiveNotificationSocket () {
     return io(this.server.url + '/live-videos')
   }
+
+  getRunnersSocket (options: {
+    runnerToken: string
+  }) {
+    return io(this.server.url + '/runners', {
+      reconnection: false,
+      auth: { runnerToken: options.runnerToken }
+    })
+  }
 }
index 3273e3a8fe8455460e582eebc03a0170362500f5..dc3c5a86ef8f45862665448648bcf47644633f00 100644 (file)
@@ -121,7 +121,7 @@ export class LiveCommand extends AbstractCommand {
     permanentLive: boolean
     privacy?: VideoPrivacy
   }) {
-    const { saveReplay, permanentLive, privacy } = options
+    const { saveReplay, permanentLive, privacy = VideoPrivacy.PUBLIC } = options
 
     const { uuid } = await this.create({
       ...options,
index 26ab2735fd33338ef19733e5c8bf6493506f35c9..7b92dcc0a385834f22842262ffccb115673b8921 100644 (file)
@@ -13,7 +13,7 @@ export class StreamingPlaylistsCommand extends AbstractCommand {
 
     withRetry?: boolean // default false
     currentRetry?: number
-  }) {
+  }): Promise<string> {
     const { videoFileToken, reinjectVideoFileToken, withRetry, currentRetry = 1 } = options
 
     try {
@@ -54,6 +54,7 @@ export class StreamingPlaylistsCommand extends AbstractCommand {
       url: options.url,
       range: options.range,
       implicitToken: false,
+      responseType: 'application/octet-stream',
       defaultExpectedStatus: HttpStatusCode.OK_200
     }))
   }
@@ -65,6 +66,7 @@ export class StreamingPlaylistsCommand extends AbstractCommand {
       ...options,
 
       url: options.url,
+      contentType: 'application/json',
       implicitToken: false,
       defaultExpectedStatus: HttpStatusCode.OK_200
     }))