bunnyhls 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,123 @@
1
+ # bunnyHLS
2
+
3
+ A full-stack npm package for BunnyNet HLS video playback with React components and Next.js API handlers.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install bunnyhls
9
+ ```
10
+
11
+ ## Features
12
+
13
+ - 🎥 React component for HLS video playback with auto-token refresh
14
+ - 🔐 Server-side URL signing handler for Next.js
15
+ - 🔄 Automatic token refresh on 403 errors
16
+ - 📦 TypeScript support
17
+ - ⚡ Works with both Next.js App Router and Pages Router
18
+
19
+ ## Usage
20
+
21
+ ### Step 1: Create the API Route
22
+
23
+ #### Next.js Pages Router (`/app/api/video-token.js`)
24
+
25
+ ```javascript
26
+ import { createBunnyHandler } from 'bunnyhls/server';
27
+
28
+ export default createBunnyHandler(process.env.BUNNY_SECURITY_KEY);
29
+ ```
30
+
31
+ #### Next.js App Router (`/app/api/video-token/route.ts`)
32
+
33
+ ```typescript
34
+ import { createBunnyHandler } from 'bunnyhls/server';
35
+ import { NextRequest } from 'next/server';
36
+
37
+ const handler = createBunnyHandler({
38
+ securityKey: process.env.BUNNY_SECURITY_KEY!,
39
+ libraryHost: process.env.LIBRARY_HOST, //eg. hvz-c561dc91-370.b-cdn.net
40
+ expirationTime: 3600, // optional, default: 3600
41
+ isDirectory: true // optional, default: true
42
+ });
43
+
44
+ export async function GET(request: NextRequest) {
45
+ return handler(request);
46
+ }
47
+ ```
48
+
49
+ ### Step 2: Use the Component
50
+
51
+ #### Next.js App Router (`/app/page.tsx`)
52
+
53
+ ```tsx
54
+ import { BunnyVideoPlayer } from 'bunnyhls/client';
55
+
56
+ export default function Page() {
57
+ return (
58
+ <BunnyVideoPlayer
59
+ videoId="7f142e98-c6df-4e3c-8ee4-9b98b6abf73f"
60
+ tokenUrl="/api/video-token"
61
+ />
62
+ );
63
+ }
64
+ ```
65
+
66
+
67
+ ## API
68
+
69
+ ### `BunnyVideoPlayer` Component
70
+
71
+ Props:
72
+
73
+ - `videoId` (string, required): The BunnyNet video ID
74
+ - `tokenUrl` (string, required): The API endpoint that returns signed URLs
75
+ - `libraryHost` (string, required): Your BunnyNet library host
76
+ - `controls` (boolean, optional): Show video controls (default: `true`)
77
+ - `style` (CSSProperties, optional): Custom styles for the video element
78
+ - `className` (string, optional): Custom CSS class for the video element
79
+
80
+ ### `createBunnyHandler` Function
81
+
82
+ Creates a handler function for your API route.
83
+
84
+ **Simple usage:**
85
+ ```javascript
86
+ createBunnyHandler(securityKey)
87
+ ```
88
+
89
+ **Advanced usage:**
90
+ ```javascript
91
+ createBunnyHandler({
92
+ securityKey: 'your-security-key',
93
+ libraryHost: 'vz-c561dc91-370.b-cdn.net',
94
+ expirationTime: 3600,
95
+ isDirectory: true
96
+ })
97
+ ```
98
+
99
+ Options:
100
+
101
+ - `securityKey` (string, required): Your BunnyNet security key
102
+ - `libraryHost` (string, optional): Your BunnyNet library host
103
+ - `expirationTime` (number, optional): Token expiration time in seconds (default: 3600)
104
+ - `isDirectory` (boolean, optional): Whether to sign as directory (default: true)
105
+
106
+ ## Environment Variables
107
+
108
+ Make sure to set your BunnyNet security key:
109
+
110
+ ```bash
111
+ BUNNY_SECURITY_KEY=your-security-key-here
112
+ ```
113
+
114
+ ## How It Works
115
+
116
+ 1. The `BunnyVideoPlayer` component requests a signed URL from your API endpoint
117
+ 2. The API handler uses your security key to sign the HLS playlist URL
118
+ 3. The component loads the signed URL using hls.js
119
+ 4. If a 403 error occurs, the component automatically requests a new token and retries
120
+
121
+ ## License
122
+
123
+ ISC
@@ -0,0 +1,12 @@
1
+ import React from 'react';
2
+
3
+ interface BunnyVideoPlayerProps {
4
+ videoId: string;
5
+ tokenUrl: string;
6
+ controls?: boolean;
7
+ style?: React.CSSProperties;
8
+ className?: string;
9
+ }
10
+ declare const BunnyVideoPlayer: React.FC<BunnyVideoPlayerProps>;
11
+
12
+ export { BunnyVideoPlayer, type BunnyVideoPlayerProps };
@@ -0,0 +1,12 @@
1
+ import React from 'react';
2
+
3
+ interface BunnyVideoPlayerProps {
4
+ videoId: string;
5
+ tokenUrl: string;
6
+ controls?: boolean;
7
+ style?: React.CSSProperties;
8
+ className?: string;
9
+ }
10
+ declare const BunnyVideoPlayer: React.FC<BunnyVideoPlayerProps>;
11
+
12
+ export { BunnyVideoPlayer, type BunnyVideoPlayerProps };
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/client/index.ts
31
+ var client_exports = {};
32
+ __export(client_exports, {
33
+ BunnyVideoPlayer: () => BunnyVideoPlayer
34
+ });
35
+ module.exports = __toCommonJS(client_exports);
36
+
37
+ // src/client/BunnyVideoPlayer.tsx
38
+ var import_react = __toESM(require("react"));
39
+ var import_hls = __toESM(require("hls.js"));
40
+ var BunnyVideoPlayer = ({
41
+ videoId,
42
+ tokenUrl,
43
+ controls = true,
44
+ style,
45
+ className
46
+ }) => {
47
+ const videoRef = (0, import_react.useRef)(null);
48
+ const hlsRef = (0, import_react.useRef)(null);
49
+ (0, import_react.useEffect)(() => {
50
+ if (!videoRef.current) return;
51
+ const hls = new import_hls.default();
52
+ hlsRef.current = hls;
53
+ const loadSourceWithToken = async () => {
54
+ try {
55
+ const res = await fetch(`${tokenUrl}?videoId=${videoId}`);
56
+ if (!res.ok) {
57
+ throw new Error(`Failed to fetch token: ${res.statusText}`);
58
+ }
59
+ const { signedUrl } = await res.json();
60
+ hls.loadSource(signedUrl);
61
+ } catch (error) {
62
+ console.error("Error loading video token:", error);
63
+ }
64
+ };
65
+ hls.attachMedia(videoRef.current);
66
+ loadSourceWithToken();
67
+ hls.on(import_hls.default.Events.ERROR, async (event, data) => {
68
+ if (data.fatal) {
69
+ switch (data.type) {
70
+ case import_hls.default.ErrorTypes.NETWORK_ERROR:
71
+ if (data.response?.code === 403) {
72
+ await loadSourceWithToken();
73
+ hls.startLoad();
74
+ } else {
75
+ hls.startLoad();
76
+ }
77
+ break;
78
+ case import_hls.default.ErrorTypes.MEDIA_ERROR:
79
+ hls.recoverMediaError();
80
+ break;
81
+ default:
82
+ hls.destroy();
83
+ break;
84
+ }
85
+ }
86
+ });
87
+ return () => {
88
+ if (hlsRef.current) {
89
+ hlsRef.current.destroy();
90
+ hlsRef.current = null;
91
+ }
92
+ };
93
+ }, [videoId, tokenUrl]);
94
+ return /* @__PURE__ */ import_react.default.createElement(
95
+ "video",
96
+ {
97
+ ref: videoRef,
98
+ controls,
99
+ style: style || { width: "100%" },
100
+ className
101
+ }
102
+ );
103
+ };
104
+ // Annotate the CommonJS export names for ESM import in node:
105
+ 0 && (module.exports = {
106
+ BunnyVideoPlayer
107
+ });
@@ -0,0 +1,70 @@
1
+ // src/client/BunnyVideoPlayer.tsx
2
+ import React, { useEffect, useRef } from "react";
3
+ import Hls from "hls.js";
4
+ var BunnyVideoPlayer = ({
5
+ videoId,
6
+ tokenUrl,
7
+ controls = true,
8
+ style,
9
+ className
10
+ }) => {
11
+ const videoRef = useRef(null);
12
+ const hlsRef = useRef(null);
13
+ useEffect(() => {
14
+ if (!videoRef.current) return;
15
+ const hls = new Hls();
16
+ hlsRef.current = hls;
17
+ const loadSourceWithToken = async () => {
18
+ try {
19
+ const res = await fetch(`${tokenUrl}?videoId=${videoId}`);
20
+ if (!res.ok) {
21
+ throw new Error(`Failed to fetch token: ${res.statusText}`);
22
+ }
23
+ const { signedUrl } = await res.json();
24
+ hls.loadSource(signedUrl);
25
+ } catch (error) {
26
+ console.error("Error loading video token:", error);
27
+ }
28
+ };
29
+ hls.attachMedia(videoRef.current);
30
+ loadSourceWithToken();
31
+ hls.on(Hls.Events.ERROR, async (event, data) => {
32
+ if (data.fatal) {
33
+ switch (data.type) {
34
+ case Hls.ErrorTypes.NETWORK_ERROR:
35
+ if (data.response?.code === 403) {
36
+ await loadSourceWithToken();
37
+ hls.startLoad();
38
+ } else {
39
+ hls.startLoad();
40
+ }
41
+ break;
42
+ case Hls.ErrorTypes.MEDIA_ERROR:
43
+ hls.recoverMediaError();
44
+ break;
45
+ default:
46
+ hls.destroy();
47
+ break;
48
+ }
49
+ }
50
+ });
51
+ return () => {
52
+ if (hlsRef.current) {
53
+ hlsRef.current.destroy();
54
+ hlsRef.current = null;
55
+ }
56
+ };
57
+ }, [videoId, tokenUrl]);
58
+ return /* @__PURE__ */ React.createElement(
59
+ "video",
60
+ {
61
+ ref: videoRef,
62
+ controls,
63
+ style: style || { width: "100%" },
64
+ className
65
+ }
66
+ );
67
+ };
68
+ export {
69
+ BunnyVideoPlayer
70
+ };
@@ -0,0 +1,9 @@
1
+ interface BunnyHandlerOptions {
2
+ securityKey: string;
3
+ libraryHost: string;
4
+ expirationTime?: number;
5
+ isDirectory?: boolean;
6
+ }
7
+ declare const createBunnyHandler: (options: BunnyHandlerOptions) => (req: any, res?: any) => Promise<any>;
8
+
9
+ export { type BunnyHandlerOptions, createBunnyHandler };
@@ -0,0 +1,9 @@
1
+ interface BunnyHandlerOptions {
2
+ securityKey: string;
3
+ libraryHost: string;
4
+ expirationTime?: number;
5
+ isDirectory?: boolean;
6
+ }
7
+ declare const createBunnyHandler: (options: BunnyHandlerOptions) => (req: any, res?: any) => Promise<any>;
8
+
9
+ export { type BunnyHandlerOptions, createBunnyHandler };
@@ -0,0 +1,182 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/server/index.ts
31
+ var server_exports = {};
32
+ __export(server_exports, {
33
+ createBunnyHandler: () => createBunnyHandler
34
+ });
35
+ module.exports = __toCommonJS(server_exports);
36
+
37
+ // src/server/signUtils.ts
38
+ var crypto = __toESM(require("crypto"));
39
+ var querystring = __toESM(require("querystring"));
40
+ function addCountries(url, countriesAllowed, countriesBlocked) {
41
+ let tempUrl = url;
42
+ if (countriesAllowed != null) {
43
+ const tempUrlOne = new URL(tempUrl);
44
+ tempUrl += (tempUrlOne.search === "" ? "?" : "&") + "token_countries=" + countriesAllowed;
45
+ }
46
+ if (countriesBlocked != null) {
47
+ const tempUrlTwo = new URL(tempUrl);
48
+ tempUrl += (tempUrlTwo.search === "" ? "?" : "&") + "token_countries_blocked=" + countriesBlocked;
49
+ }
50
+ return tempUrl;
51
+ }
52
+ function signUrl(url, securityKey, expirationTime = 3600, userIp = null, isDirectory = false, pathAllowed = "", countriesAllowed = null, countriesBlocked = null) {
53
+ console.log("[BunnyHLS signUrl] Starting URL signing process");
54
+ console.log("[BunnyHLS signUrl] Input URL:", url);
55
+ console.log("[BunnyHLS signUrl] Parameters:", {
56
+ expirationTime,
57
+ userIp: userIp || "(none)",
58
+ isDirectory,
59
+ pathAllowed: pathAllowed || "(none)",
60
+ countriesAllowed: countriesAllowed || "(none)",
61
+ countriesBlocked: countriesBlocked || "(none)"
62
+ });
63
+ let parameterData = "";
64
+ let parameterDataUrl = "";
65
+ let signaturePath = "";
66
+ let hashableBase = "";
67
+ let token = "";
68
+ const expires = Math.floor((/* @__PURE__ */ new Date()).getTime() / 1e3) + expirationTime;
69
+ console.log("[BunnyHLS signUrl] Expiration timestamp:", expires, `(${expirationTime}s from now)`);
70
+ const urlWithCountries = addCountries(url, countriesAllowed, countriesBlocked);
71
+ const parsedUrl = new URL(urlWithCountries);
72
+ const parameters = new URL(urlWithCountries).searchParams;
73
+ if (pathAllowed !== "") {
74
+ signaturePath = pathAllowed;
75
+ parameters.set("token_path", signaturePath);
76
+ console.log("[BunnyHLS signUrl] Using pathAllowed:", signaturePath);
77
+ } else {
78
+ signaturePath = decodeURIComponent(parsedUrl.pathname);
79
+ console.log("[BunnyHLS signUrl] Using parsed pathname:", signaturePath);
80
+ }
81
+ const sortedParams = Array.from(parameters.entries()).sort();
82
+ console.log("[BunnyHLS signUrl] Sorted parameters:", sortedParams);
83
+ sortedParams.forEach(([key, value]) => {
84
+ if (value === "") {
85
+ return;
86
+ }
87
+ if (parameterData.length > 0) {
88
+ parameterData += "&";
89
+ }
90
+ parameterData += key + "=" + value;
91
+ parameterDataUrl += "&" + key + "=" + querystring.escape(value);
92
+ });
93
+ console.log("[BunnyHLS signUrl] Parameter data:", parameterData);
94
+ console.log("[BunnyHLS signUrl] Parameter data URL:", parameterDataUrl);
95
+ hashableBase = securityKey + signaturePath + expires + (userIp != null ? userIp : "") + parameterData;
96
+ console.log("[BunnyHLS signUrl] Hashable base length:", hashableBase.length);
97
+ console.log("[BunnyHLS signUrl] Hashable base preview:", hashableBase.substring(0, 50) + "...");
98
+ token = Buffer.from(crypto.createHash("sha256").update(hashableBase).digest()).toString("base64");
99
+ token = token.replace(/\n/g, "").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
100
+ console.log("[BunnyHLS signUrl] Generated token:", token.substring(0, 20) + "...");
101
+ let finalUrl;
102
+ if (isDirectory) {
103
+ finalUrl = parsedUrl.protocol + "//" + parsedUrl.host + "/bcdn_token=" + token + parameterDataUrl + "&expires=" + expires + parsedUrl.pathname;
104
+ console.log("[BunnyHLS signUrl] Directory mode - Final URL generated");
105
+ } else {
106
+ finalUrl = parsedUrl.protocol + "//" + parsedUrl.host + parsedUrl.pathname + "?token=" + token + parameterDataUrl + "&expires=" + expires;
107
+ console.log("[BunnyHLS signUrl] File mode - Final URL generated");
108
+ }
109
+ console.log("[BunnyHLS signUrl] Signing complete");
110
+ return finalUrl;
111
+ }
112
+
113
+ // src/server/index.ts
114
+ var createBunnyHandler = (options) => {
115
+ const {
116
+ securityKey,
117
+ libraryHost,
118
+ expirationTime = 3600,
119
+ isDirectory = true
120
+ } = options;
121
+ return async (req, res) => {
122
+ console.log("[BunnyHLS] Handler called at", (/* @__PURE__ */ new Date()).toISOString());
123
+ let videoId;
124
+ if (req.nextUrl) {
125
+ videoId = req.nextUrl.searchParams.get("videoId") || "";
126
+ console.log("[BunnyHLS] App Router - Request URL:", req.nextUrl.toString());
127
+ } else if (req.query) {
128
+ videoId = req.query.videoId || "";
129
+ console.log("[BunnyHLS] Pages Router - Query params:", req.query);
130
+ } else {
131
+ videoId = "";
132
+ console.log("[BunnyHLS] No videoId found in request");
133
+ }
134
+ console.log("[BunnyHLS] Extracted videoId:", videoId || "(empty)");
135
+ if (!videoId) {
136
+ console.error("[BunnyHLS] ERROR: videoId is required");
137
+ const errorResponse = { error: "videoId is required" };
138
+ if (res) {
139
+ return res.status(400).json(errorResponse);
140
+ }
141
+ return new Response(JSON.stringify(errorResponse), {
142
+ status: 400,
143
+ headers: { "Content-Type": "application/json" }
144
+ });
145
+ }
146
+ const pathAllowed = `/${videoId}/`;
147
+ const url = `https://${libraryHost}/${videoId}/playlist.m3u8`;
148
+ console.log("[BunnyHLS] Signing URL:", url);
149
+ console.log("[BunnyHLS] Config:", {
150
+ libraryHost,
151
+ expirationTime,
152
+ isDirectory,
153
+ pathAllowed,
154
+ securityKeyLength: securityKey?.length || 0
155
+ });
156
+ const signedUrl = signUrl(
157
+ url,
158
+ securityKey,
159
+ expirationTime,
160
+ null,
161
+ isDirectory,
162
+ pathAllowed,
163
+ null,
164
+ null
165
+ );
166
+ console.log("[BunnyHLS] Signed URL generated:", signedUrl);
167
+ console.log("[BunnyHLS] Response prepared successfully");
168
+ const response = { signedUrl };
169
+ if (res) {
170
+ return res.status(200).json(response);
171
+ } else {
172
+ return new Response(JSON.stringify(response), {
173
+ status: 200,
174
+ headers: { "Content-Type": "application/json" }
175
+ });
176
+ }
177
+ };
178
+ };
179
+ // Annotate the CommonJS export names for ESM import in node:
180
+ 0 && (module.exports = {
181
+ createBunnyHandler
182
+ });
@@ -0,0 +1,145 @@
1
+ // src/server/signUtils.ts
2
+ import * as crypto from "crypto";
3
+ import * as querystring from "querystring";
4
+ function addCountries(url, countriesAllowed, countriesBlocked) {
5
+ let tempUrl = url;
6
+ if (countriesAllowed != null) {
7
+ const tempUrlOne = new URL(tempUrl);
8
+ tempUrl += (tempUrlOne.search === "" ? "?" : "&") + "token_countries=" + countriesAllowed;
9
+ }
10
+ if (countriesBlocked != null) {
11
+ const tempUrlTwo = new URL(tempUrl);
12
+ tempUrl += (tempUrlTwo.search === "" ? "?" : "&") + "token_countries_blocked=" + countriesBlocked;
13
+ }
14
+ return tempUrl;
15
+ }
16
+ function signUrl(url, securityKey, expirationTime = 3600, userIp = null, isDirectory = false, pathAllowed = "", countriesAllowed = null, countriesBlocked = null) {
17
+ console.log("[BunnyHLS signUrl] Starting URL signing process");
18
+ console.log("[BunnyHLS signUrl] Input URL:", url);
19
+ console.log("[BunnyHLS signUrl] Parameters:", {
20
+ expirationTime,
21
+ userIp: userIp || "(none)",
22
+ isDirectory,
23
+ pathAllowed: pathAllowed || "(none)",
24
+ countriesAllowed: countriesAllowed || "(none)",
25
+ countriesBlocked: countriesBlocked || "(none)"
26
+ });
27
+ let parameterData = "";
28
+ let parameterDataUrl = "";
29
+ let signaturePath = "";
30
+ let hashableBase = "";
31
+ let token = "";
32
+ const expires = Math.floor((/* @__PURE__ */ new Date()).getTime() / 1e3) + expirationTime;
33
+ console.log("[BunnyHLS signUrl] Expiration timestamp:", expires, `(${expirationTime}s from now)`);
34
+ const urlWithCountries = addCountries(url, countriesAllowed, countriesBlocked);
35
+ const parsedUrl = new URL(urlWithCountries);
36
+ const parameters = new URL(urlWithCountries).searchParams;
37
+ if (pathAllowed !== "") {
38
+ signaturePath = pathAllowed;
39
+ parameters.set("token_path", signaturePath);
40
+ console.log("[BunnyHLS signUrl] Using pathAllowed:", signaturePath);
41
+ } else {
42
+ signaturePath = decodeURIComponent(parsedUrl.pathname);
43
+ console.log("[BunnyHLS signUrl] Using parsed pathname:", signaturePath);
44
+ }
45
+ const sortedParams = Array.from(parameters.entries()).sort();
46
+ console.log("[BunnyHLS signUrl] Sorted parameters:", sortedParams);
47
+ sortedParams.forEach(([key, value]) => {
48
+ if (value === "") {
49
+ return;
50
+ }
51
+ if (parameterData.length > 0) {
52
+ parameterData += "&";
53
+ }
54
+ parameterData += key + "=" + value;
55
+ parameterDataUrl += "&" + key + "=" + querystring.escape(value);
56
+ });
57
+ console.log("[BunnyHLS signUrl] Parameter data:", parameterData);
58
+ console.log("[BunnyHLS signUrl] Parameter data URL:", parameterDataUrl);
59
+ hashableBase = securityKey + signaturePath + expires + (userIp != null ? userIp : "") + parameterData;
60
+ console.log("[BunnyHLS signUrl] Hashable base length:", hashableBase.length);
61
+ console.log("[BunnyHLS signUrl] Hashable base preview:", hashableBase.substring(0, 50) + "...");
62
+ token = Buffer.from(crypto.createHash("sha256").update(hashableBase).digest()).toString("base64");
63
+ token = token.replace(/\n/g, "").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
64
+ console.log("[BunnyHLS signUrl] Generated token:", token.substring(0, 20) + "...");
65
+ let finalUrl;
66
+ if (isDirectory) {
67
+ finalUrl = parsedUrl.protocol + "//" + parsedUrl.host + "/bcdn_token=" + token + parameterDataUrl + "&expires=" + expires + parsedUrl.pathname;
68
+ console.log("[BunnyHLS signUrl] Directory mode - Final URL generated");
69
+ } else {
70
+ finalUrl = parsedUrl.protocol + "//" + parsedUrl.host + parsedUrl.pathname + "?token=" + token + parameterDataUrl + "&expires=" + expires;
71
+ console.log("[BunnyHLS signUrl] File mode - Final URL generated");
72
+ }
73
+ console.log("[BunnyHLS signUrl] Signing complete");
74
+ return finalUrl;
75
+ }
76
+
77
+ // src/server/index.ts
78
+ var createBunnyHandler = (options) => {
79
+ const {
80
+ securityKey,
81
+ libraryHost,
82
+ expirationTime = 3600,
83
+ isDirectory = true
84
+ } = options;
85
+ return async (req, res) => {
86
+ console.log("[BunnyHLS] Handler called at", (/* @__PURE__ */ new Date()).toISOString());
87
+ let videoId;
88
+ if (req.nextUrl) {
89
+ videoId = req.nextUrl.searchParams.get("videoId") || "";
90
+ console.log("[BunnyHLS] App Router - Request URL:", req.nextUrl.toString());
91
+ } else if (req.query) {
92
+ videoId = req.query.videoId || "";
93
+ console.log("[BunnyHLS] Pages Router - Query params:", req.query);
94
+ } else {
95
+ videoId = "";
96
+ console.log("[BunnyHLS] No videoId found in request");
97
+ }
98
+ console.log("[BunnyHLS] Extracted videoId:", videoId || "(empty)");
99
+ if (!videoId) {
100
+ console.error("[BunnyHLS] ERROR: videoId is required");
101
+ const errorResponse = { error: "videoId is required" };
102
+ if (res) {
103
+ return res.status(400).json(errorResponse);
104
+ }
105
+ return new Response(JSON.stringify(errorResponse), {
106
+ status: 400,
107
+ headers: { "Content-Type": "application/json" }
108
+ });
109
+ }
110
+ const pathAllowed = `/${videoId}/`;
111
+ const url = `https://${libraryHost}/${videoId}/playlist.m3u8`;
112
+ console.log("[BunnyHLS] Signing URL:", url);
113
+ console.log("[BunnyHLS] Config:", {
114
+ libraryHost,
115
+ expirationTime,
116
+ isDirectory,
117
+ pathAllowed,
118
+ securityKeyLength: securityKey?.length || 0
119
+ });
120
+ const signedUrl = signUrl(
121
+ url,
122
+ securityKey,
123
+ expirationTime,
124
+ null,
125
+ isDirectory,
126
+ pathAllowed,
127
+ null,
128
+ null
129
+ );
130
+ console.log("[BunnyHLS] Signed URL generated:", signedUrl);
131
+ console.log("[BunnyHLS] Response prepared successfully");
132
+ const response = { signedUrl };
133
+ if (res) {
134
+ return res.status(200).json(response);
135
+ } else {
136
+ return new Response(JSON.stringify(response), {
137
+ status: 200,
138
+ headers: { "Content-Type": "application/json" }
139
+ });
140
+ }
141
+ };
142
+ };
143
+ export {
144
+ createBunnyHandler
145
+ };
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "bunnyhls",
3
+ "version": "1.0.0",
4
+ "description": "Full-stack npm package for BunnyNet HLS video playback with React components and Next.js API handlers",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "private": false,
8
+ "exports": {
9
+ ".": "./dist/index.js",
10
+ "./client": "./dist/client/index.js",
11
+ "./server": "./dist/server/index.js"
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "README.md",
16
+ "LICENSE"
17
+ ],
18
+ "scripts": {
19
+ "start": "node sign.js",
20
+ "build": "tsup src/client/index.ts src/server/index.ts --format cjs,esm --dts --clean",
21
+ "prepublishOnly": "npm run build"
22
+ },
23
+ "author": "Samuel Petros Kelbiso (sampk7)",
24
+ "license": "ISC",
25
+ "dependencies": {
26
+ "hls.js": "^1.4.12"
27
+ },
28
+ "devDependencies": {
29
+ "@types/node": "^20.0.0",
30
+ "@types/react": "^18.0.0",
31
+ "typescript": "^5.0.0",
32
+ "tsup": "^8.0.0"
33
+ },
34
+ "peerDependencies": {
35
+ "react": "^18.0.0 || ^19.0.0",
36
+ "react-dom": "^18.0.0 || ^19.0.0"
37
+ },
38
+ "peerDependenciesMeta": {
39
+ "react-dom": {
40
+ "optional": false
41
+ }
42
+ }
43
+ }