diff --git a/examples/default-provider/videos/storage.googleapis.com_muxdemofiles_mux.mp4.json b/examples/default-provider/videos/storage.googleapis.com_muxdemofiles_mux.mp4.json
index 2b1f1af..f275c44 100644
--- a/examples/default-provider/videos/storage.googleapis.com_muxdemofiles_mux.mp4.json
+++ b/examples/default-provider/videos/storage.googleapis.com_muxdemofiles_mux.mp4.json
@@ -1 +1 @@
-{"status":"ready","originalFilePath":"https://storage.googleapis.com/muxdemofiles/mux.mp4","provider":"mux","providerMetadata":{"mux":{"assetId":"EploFGgmKULMpiyDFwsy5c6lmGcg8dkObaVvnPMcdkQ","playbackId":"jxEf6XiJs6JY017pSzpv8Hd6tTbdAOecHTq4FiFAn564"}},"createdAt":1710979438730,"updatedAt":1710979441038,"sources":[{"src":"https://stream.mux.com/.m3u8","type":"application/x-mpegURL"}],"poster":"https://image.mux.com//thumbnail.webp","blurDataURL":"data:image/webp;base64,UklGRlAAAABXRUJQVlA4IEQAAACwAQCdASoQAAkAAQAcJZwAAueBHFYwAP7+sPJ01xp5AM+XuhDsRQ67ZYXXhHDkrqsIkUGjQSCMuENc5y3Qg0o9pZgAAA=="}
+{"status":"ready","originalFilePath":"https://storage.googleapis.com/muxdemofiles/mux.mp4","provider":"mux","providerMetadata":{"mux":{"assetId":"EploFGgmKULMpiyDFwsy5c6lmGcg8dkObaVvnPMcdkQ","playbackId":"jxEf6XiJs6JY017pSzpv8Hd6tTbdAOecHTq4FiFAn564"}},"createdAt":1710979438730,"updatedAt":1710979441038,"sources":[{"src":"https://stream.mux.com/jxEf6XiJs6JY017pSzpv8Hd6tTbdAOecHTq4FiFAn564.m3u8","type":"application/x-mpegURL"}],"poster":"https://image.mux.com//thumbnail.webp","blurDataURL":"data:image/webp;base64,UklGRlAAAABXRUJQVlA4IEQAAACwAQCdASoQAAkAAQAcJZwAAueBHFYwAP7+sPJ01xp5AM+XuhDsRQ67ZYXXhHDkrqsIkUGjQSCMuENc5y3Qg0o9pZgAAA=="}
diff --git a/src/components/video.tsx b/src/components/video.tsx
index 9a2c649..1832ac6 100644
--- a/src/components/video.tsx
+++ b/src/components/video.tsx
@@ -11,9 +11,10 @@ import type { DefaultPlayerProps } from './players/default-player.js';
import type { Asset } from '../assets.js';
import type { VideoLoaderProps, VideoProps, VideoPropsInternal } from './types.js';
-const DEV_MODE = process.env.NODE_ENV === 'development';
-
const NextVideo = forwardRef((props: VideoProps, forwardedRef) => {
+ // Keep in component so we can emulate the DEV_MODE.
+ const DEV_MODE = process.env.NODE_ENV === 'development';
+
let {
as: VideoPlayer = DefaultPlayer,
loader = defaultLoader,
diff --git a/src/handlers/api-request.ts b/src/handlers/api-request.ts
index e9c8cf8..02bb745 100644
--- a/src/handlers/api-request.ts
+++ b/src/handlers/api-request.ts
@@ -1,3 +1,4 @@
+/* c8 ignore start */
import * as providers from '../providers/providers.js';
import { camelCase } from '../utils/utils.js';
import type { Asset } from '../assets.js';
@@ -10,3 +11,4 @@ export async function uploadRequestedFile(asset: Asset, config: HandlerConfig) {
}
}
}
+/* c8 ignore stop */
diff --git a/src/providers/mux/transformer.ts b/src/providers/mux/transformer.ts
index 5824541..cf23270 100644
--- a/src/providers/mux/transformer.ts
+++ b/src/providers/mux/transformer.ts
@@ -26,7 +26,10 @@ export function transform(asset: Asset, props?: Props) {
const transformedAsset: Asset = {
...asset,
- sources: [{ src: `https://stream.mux.com/${playbackId}.m3u8`, type: 'application/x-mpegURL' }],
+ sources: [{
+ src: `https://stream.${props?.customDomain ?? MUX_VIDEO_DOMAIN}/${playbackId}.m3u8`,
+ type: 'application/x-mpegURL'
+ }],
poster: getPosterURLFromPlaybackId(playbackId, {
thumbnailTime,
diff --git a/src/utils/logger.ts b/src/utils/logger.ts
index e6939cc..e6f2146 100644
--- a/src/utils/logger.ts
+++ b/src/utils/logger.ts
@@ -1,3 +1,4 @@
+/* c8 ignore start */
import chalk from 'chalk';
type logType = 'log' | 'error';
@@ -47,3 +48,4 @@ export default {
space,
label,
};
+/* c8 ignore stop */
diff --git a/src/utils/s3.ts b/src/utils/s3.ts
index 605a591..88b5a87 100644
--- a/src/utils/s3.ts
+++ b/src/utils/s3.ts
@@ -1,3 +1,4 @@
+/* c8 ignore start */
import {
S3Client,
PutBucketCorsCommand,
@@ -62,3 +63,4 @@ export function putBucketCors(s3: S3Client, bucketName: string) {
},
}));
}
+/* c8 ignore stop */
diff --git a/tests/components/alert.test.tsx b/tests/components/alert.test.tsx
new file mode 100644
index 0000000..3d3e877
--- /dev/null
+++ b/tests/components/alert.test.tsx
@@ -0,0 +1,24 @@
+import assert from 'node:assert';
+import { test } from 'node:test';
+import { setTimeout } from 'node:timers/promises';
+import { create } from 'react-test-renderer';
+import React from 'react';
+import { Alert } from '../../src/components/alert.js';
+
+test('renders an error alert', async () => {
+ const wrapper = create();
+ await setTimeout(50);
+ const fragment = wrapper.toJSON();
+ assert.equal(fragment[1].type, 'div');
+ assert.equal(fragment[1].props.className, 'next-video-alert next-video-alert-error');
+ assert.equal(fragment[1].props.hidden, true);
+});
+
+test('renders a sourced alert', async () => {
+ const wrapper = create();
+ await setTimeout(50);
+ const fragment = wrapper.toJSON();
+ assert.equal(fragment[1].type, 'div');
+ assert.equal(fragment[1].props.className, 'next-video-alert next-video-alert-sourced');
+ assert.equal(fragment[1].props.hidden, false);
+});
diff --git a/tests/components/utils.test.tsx b/tests/components/utils.test.tsx
new file mode 100644
index 0000000..711a7b8
--- /dev/null
+++ b/tests/components/utils.test.tsx
@@ -0,0 +1,24 @@
+import assert from 'node:assert';
+import { test } from 'node:test';
+import React from 'react';
+import { isReactComponent, getUrlExtension } from '../../src/components/utils.js';
+
+test('isReactComponent', () => {
+ assert.ok(isReactComponent(() => null), 'function component');
+ assert.ok(isReactComponent(class extends React.Component {}), 'class component');
+ assert.ok(isReactComponent(React.memo(() => null)), 'memo');
+ assert.ok(isReactComponent(React.forwardRef(() => null)), 'forwardRef');
+});
+
+test('getUrlExtension', () => {
+ assert.strictEqual(getUrlExtension('https://example.com/image.jpg'), 'jpg');
+ assert.strictEqual(getUrlExtension('https://example.com/image.jpg?foo=bar'), 'jpg');
+ assert.strictEqual(getUrlExtension('https://example.com/image.jpg#foo'), 'jpg');
+ assert.strictEqual(getUrlExtension('https://example.com/image.jpg?foo=bar#foo'), 'jpg');
+ assert.strictEqual(getUrlExtension('https://example.com/image.jpg?foo=bar&baz=qux'), 'jpg');
+ assert.strictEqual(getUrlExtension('https://example.com/image.jpg?foo=bar&baz=qux#foo'), 'jpg');
+ assert.strictEqual(getUrlExtension('https://example.com/image.jpg#foo?foo=bar&baz=qux'), 'jpg');
+ assert.strictEqual(getUrlExtension('https://example.com/image.jpg#foo?foo=bar&baz=qux#foo'), 'jpg');
+ assert.strictEqual(getUrlExtension('https://example.com/image.jpg?foo=bar&baz=qux#foo?foo=bar&baz=qux'), 'jpg');
+ assert.strictEqual(getUrlExtension('https://example.com/image.jpg?foo=bar&baz=qux#foo?foo=bar&baz=qux#foo'), 'jpg');
+});
diff --git a/tests/components/video-loader.test.ts b/tests/components/video-loader.test.ts
new file mode 100644
index 0000000..8e1210a
--- /dev/null
+++ b/tests/components/video-loader.test.ts
@@ -0,0 +1,25 @@
+import assert from 'node:assert';
+import { test, mock } from 'node:test';
+import { defaultLoader, createVideoRequest } from '../../src/components/video-loader.js';
+
+test('createVideoRequest', async () => {
+
+ mock.method(global, 'fetch', () => {
+ return { ok: true, status: 200, json: async () => ({ status: 'ready' }) };
+ });
+
+ const loader = ({ config, src, width, height }: any) => {
+ config.path = 'https://example.com/api/video';
+ return defaultLoader({ config, src, width, height });
+ };
+
+ const props = { src: 'https://example.com/video.mp4' };
+ const callback = (json) => {
+ assert.equal(json.status, 'ready');
+ };
+
+ const request = createVideoRequest(loader, props, callback);
+ await request(new AbortController().signal);
+
+ mock.reset();
+});
diff --git a/tests/components/video.test.tsx b/tests/components/video.test.tsx
index b52cb8d..77bddfc 100644
--- a/tests/components/video.test.tsx
+++ b/tests/components/video.test.tsx
@@ -1,5 +1,5 @@
import assert from 'node:assert';
-import { test } from 'node:test';
+import { test, mock } from 'node:test';
import { setTimeout } from 'node:timers/promises';
import { create } from 'react-test-renderer';
import React from 'react';
@@ -36,3 +36,61 @@ test('renders mux-player with imported source', async () => {
'zNYmqdvJ61gt5uip02zPid01rYIPyyzVRVKQChgSgJlaY'
);
});
+
+test('renders mux-player with string source', async () => {
+ await import('@mux/mux-player-react');
+
+ process.env.NODE_ENV = 'development';
+
+ let keepalive = globalThis.setTimeout(() => {}, 5_000);
+
+ let resolve;
+ const pollReady = new Promise((res) => {
+ resolve = res;
+ });
+
+ let count = 0;
+
+ mock.method(global, 'fetch', async () => {
+ return {
+ ok: true,
+ status: 200,
+ json: async () => {
+ count++;
+
+ if (count < 2) {
+ return {
+ status: 'uploading',
+ provider: 'mux',
+ };
+ }
+
+ resolve();
+
+ return {
+ status: 'ready',
+ provider: 'mux',
+ sources: [{
+ type: 'application/x-mpegURL',
+ src: 'https://stream.mux.com/jxEf6XiJs6JY017pSzpv8Hd6tTbdAOecHTq4FiFAn564.m3u8'
+ }]
+ };
+ }
+ };
+ });
+
+ const wrapper = create();
+
+ await pollReady;
+ await setTimeout(50);
+
+ clearTimeout(keepalive);
+
+ assert.equal(wrapper.toJSON().children[1].type, 'mux-player');
+ assert.equal(
+ wrapper.root.findByType('mux-player').parent.parent.props.src,
+ 'https://stream.mux.com/jxEf6XiJs6JY017pSzpv8Hd6tTbdAOecHTq4FiFAn564.m3u8'
+ );
+
+ mock.reset();
+});
diff --git a/tests/providers/amazon-s3/transformer.test.ts b/tests/providers/amazon-s3/transformer.test.ts
new file mode 100644
index 0000000..dcdd308
--- /dev/null
+++ b/tests/providers/amazon-s3/transformer.test.ts
@@ -0,0 +1,28 @@
+import assert from 'node:assert';
+import { test } from 'node:test';
+import { transform } from '../../../src/providers/amazon-s3/transformer.js';
+import type { Asset } from '../../../src/assets.js';
+
+test('transform', async () => {
+ const asset: Asset = {
+ status: 'ready',
+ originalFilePath: '/videos/get-started.mp4',
+ createdAt: 0,
+ updatedAt: 0,
+ provider: 'amazon-s3',
+ providerMetadata: {
+ ['amazon-s3']: {
+ endpoint: 'https://amazon-s3-url.com',
+ bucket: 'bucket',
+ key: 'key',
+ },
+ },
+ };
+
+ const transformedAsset = transform(asset);
+
+ assert.deepStrictEqual(transformedAsset, {
+ ...asset,
+ sources: [{ src: 'https://bucket.amazon-s3-url.com/key' }],
+ });
+});
diff --git a/tests/providers/backblaze/transformer.test.ts b/tests/providers/backblaze/transformer.test.ts
new file mode 100644
index 0000000..d3df790
--- /dev/null
+++ b/tests/providers/backblaze/transformer.test.ts
@@ -0,0 +1,28 @@
+import assert from 'node:assert';
+import { test } from 'node:test';
+import { transform } from '../../../src/providers/backblaze/transformer.js';
+import type { Asset } from '../../../src/assets.js';
+
+test('transform', async () => {
+ const asset: Asset = {
+ status: 'ready',
+ originalFilePath: '/videos/get-started.mp4',
+ createdAt: 0,
+ updatedAt: 0,
+ provider: 'backblaze',
+ providerMetadata: {
+ ['backblaze']: {
+ endpoint: 'https://backblaze-url.com',
+ bucket: 'bucket',
+ key: 'key',
+ },
+ },
+ };
+
+ const transformedAsset = transform(asset);
+
+ assert.deepStrictEqual(transformedAsset, {
+ ...asset,
+ sources: [{ src: 'https://bucket.backblaze-url.com/key' }],
+ });
+});
diff --git a/tests/providers/mux/transformer.test.ts b/tests/providers/mux/transformer.test.ts
new file mode 100644
index 0000000..979ed9a
--- /dev/null
+++ b/tests/providers/mux/transformer.test.ts
@@ -0,0 +1,31 @@
+import assert from 'node:assert';
+import { test } from 'node:test';
+import { transform } from '../../../src/providers/mux/transformer.js';
+import type { Asset } from '../../../src/assets.js';
+
+test('transform', async () => {
+ const asset: Asset = {
+ status: 'ready',
+ originalFilePath: '/videos/get-started.mp4',
+ createdAt: 0,
+ updatedAt: 0,
+ provider: 'mux',
+ providerMetadata: {
+ mux: {
+ playbackId: 'playbackId',
+ },
+ },
+ };
+
+ const transformedAsset = transform(asset, {
+ customDomain: 'custom-mux.com',
+ thumbnailTime: 20,
+ });
+
+ assert.deepStrictEqual(transformedAsset, {
+ ...asset,
+ sources: [{ src: 'https://stream.custom-mux.com/playbackId.m3u8', type: 'application/x-mpegURL' }],
+ poster: 'https://image.custom-mux.com/playbackId/thumbnail.webp?time=20',
+ thumbnailTime: 20,
+ });
+});
diff --git a/tests/providers/vercel-blob/transformer.test.ts b/tests/providers/vercel-blob/transformer.test.ts
new file mode 100644
index 0000000..bd2b1f2
--- /dev/null
+++ b/tests/providers/vercel-blob/transformer.test.ts
@@ -0,0 +1,27 @@
+import assert from 'node:assert';
+import { test } from 'node:test';
+import { transform } from '../../../src/providers/vercel-blob/transformer.js';
+import type { Asset } from '../../../src/assets.js';
+
+test('transform', async () => {
+ const asset: Asset = {
+ status: 'ready',
+ originalFilePath: '/videos/get-started.mp4',
+ createdAt: 0,
+ updatedAt: 0,
+ provider: 'vercel-blob',
+ providerMetadata: {
+ ['vercel-blob']: {
+ url: 'https://vercel-blob-url.com/get-started.mp4',
+ contentType: 'video/mp4',
+ },
+ },
+ };
+
+ const transformedAsset = transform(asset);
+
+ assert.deepStrictEqual(transformedAsset, {
+ ...asset,
+ sources: [{ src: 'https://vercel-blob-url.com/get-started.mp4', type: 'video/mp4' }],
+ });
+});
diff --git a/tests/utils/provider.test.ts b/tests/utils/provider.test.ts
new file mode 100644
index 0000000..a84e3c3
--- /dev/null
+++ b/tests/utils/provider.test.ts
@@ -0,0 +1,69 @@
+import assert from 'node:assert';
+import { test } from 'node:test';
+import { setConfig } from 'next/config.js'
+import { createAssetKey } from '../../src/utils/provider.js';
+
+test('createAssetKey w/ defaultGenerateAssetKey and local asset', async () => {
+ setConfig({
+ serverRuntimeConfig: {
+ nextVideo: {
+ folder: 'videos',
+ providerConfig: {
+ 'vercel-blob': {},
+ },
+ },
+ },
+ });
+
+ assert.equal(
+ await createAssetKey('/videos/get-started.mp4', 'vercel-blob'),
+ '/videos/get-started.mp4'
+ );
+
+ setConfig({});
+});
+
+test('createAssetKey w/ defaultGenerateAssetKey and remote asset', async () => {
+ setConfig({
+ serverRuntimeConfig: {
+ nextVideo: {
+ folder: 'videos',
+ providerConfig: {
+ 'vercel-blob': {},
+ },
+ },
+ },
+ });
+
+ assert.equal(
+ await createAssetKey('https://storage.googleapis.com/muxdemofiles/mux.mp4', 'vercel-blob'),
+ 'videos/mux.mp4'
+ );
+
+ setConfig({});
+});
+
+test('createAssetKey w/ custom generateAssetKey and remote asset', async () => {
+ setConfig({
+ serverRuntimeConfig: {
+ nextVideo: {
+ folder: 'videos',
+ providerConfig: {
+ 'vercel-blob': {
+ generateAssetKey: (filePathOrURL, folder) => {
+ const url = new URL(filePathOrURL);
+ return `${folder}/remote${url.pathname}`;
+ },
+ },
+ },
+ },
+ },
+ });
+
+ assert.equal(
+ await createAssetKey('https://storage.googleapis.com/muxdemofiles/mux.mp4', 'vercel-blob'),
+ 'videos/remote/muxdemofiles/mux.mp4'
+ );
+
+ setConfig({});
+});
diff --git a/tests/with-next-video.test.ts b/tests/with-next-video.test.ts
index efd0420..92088b1 100644
--- a/tests/with-next-video.test.ts
+++ b/tests/with-next-video.test.ts
@@ -47,4 +47,41 @@ describe('withNextVideo', () => {
assert(typeof result.webpack === 'function');
});
+
+ it('should handle videoConfig being passed', async () => {
+ const nextConfig = {};
+
+ const result = await withNextVideo(nextConfig, {
+ path: '/api/video-files',
+ folder: 'video-files',
+ provider: 'vercel-blob',
+ });
+
+ assert.deepEqual(result.serverRuntimeConfig.nextVideo, {
+ path: '/api/video-files',
+ folder: 'video-files',
+ provider: 'vercel-blob',
+ providerConfig: {},
+ });
+ });
+
+ it('should change the webpack config', async () => {
+ const nextConfig = {};
+ const result = await withNextVideo(nextConfig);
+ const config = {
+ externals: [],
+ experiments: {},
+ module: {
+ rules: [],
+ },
+ };
+ const options = {
+ defaultLoaders: true,
+ };
+ const webpackConfig = result.webpack(config, options);
+
+ assert.equal(webpackConfig.externals[0].sharp, 'commonjs sharp');
+ assert.equal(webpackConfig.module.rules.length, 2);
+ assert.deepEqual(webpackConfig.infrastructureLogging, { level: 'error' });
+ });
});