unified-video-framework 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/.github/workflows/ci.yml +253 -0
- package/ANDROID_TV_IMPLEMENTATION.md +313 -0
- package/COMPLETION_STATUS.md +165 -0
- package/CONTRIBUTING.md +376 -0
- package/FINAL_STATUS_REPORT.md +170 -0
- package/FRAMEWORK_REVIEW.md +247 -0
- package/IMPROVEMENTS_SUMMARY.md +168 -0
- package/LICENSE +21 -0
- package/NATIVE_APP_INTEGRATION_GUIDE.md +903 -0
- package/PAYWALL_RENTAL_FLOW.md +499 -0
- package/PLATFORM_SETUP_GUIDE.md +1636 -0
- package/README.md +315 -0
- package/RUN_LOCALLY.md +151 -0
- package/apps/demo/cast-sender-min.html +173 -0
- package/apps/demo/custom-player.html +883 -0
- package/apps/demo/demo.html +990 -0
- package/apps/demo/enhanced-player.html +3556 -0
- package/apps/demo/index.html +159 -0
- package/apps/rental-api/.env.example +24 -0
- package/apps/rental-api/README.md +23 -0
- package/apps/rental-api/migrations/001_init.sql +35 -0
- package/apps/rental-api/migrations/002_videos.sql +10 -0
- package/apps/rental-api/migrations/003_add_gateway_subref.sql +4 -0
- package/apps/rental-api/migrations/004_update_gateways.sql +4 -0
- package/apps/rental-api/migrations/005_seed_demo_video.sql +5 -0
- package/apps/rental-api/package-lock.json +2045 -0
- package/apps/rental-api/package.json +33 -0
- package/apps/rental-api/scripts/run-migration.js +42 -0
- package/apps/rental-api/scripts/update-video-currency.js +21 -0
- package/apps/rental-api/scripts/update-video-price.js +19 -0
- package/apps/rental-api/src/config.ts +14 -0
- package/apps/rental-api/src/db.ts +10 -0
- package/apps/rental-api/src/routes/cashfree.ts +167 -0
- package/apps/rental-api/src/routes/pesapal.ts +92 -0
- package/apps/rental-api/src/routes/rentals.ts +242 -0
- package/apps/rental-api/src/routes/webhooks.ts +73 -0
- package/apps/rental-api/src/server.ts +41 -0
- package/apps/rental-api/src/services/entitlements.ts +45 -0
- package/apps/rental-api/src/services/payments.ts +22 -0
- package/apps/rental-api/tsconfig.json +17 -0
- package/check-urls.ps1 +74 -0
- package/comparison-report.md +181 -0
- package/docs/PAYWALL.md +95 -0
- package/docs/PLAYER_UI_VISIBILITY.md +431 -0
- package/docs/README.md +7 -0
- package/docs/SYSTEM_ARCHITECTURE.md +612 -0
- package/docs/VDOCIPHER_CLONE_REQUIREMENTS.md +403 -0
- package/examples/android/JavaSampleApp/MainActivity.java +641 -0
- package/examples/android/JavaSampleApp/activity_main.xml +226 -0
- package/examples/android/SampleApp/MainActivity.kt +430 -0
- package/examples/ios/SampleApp/ViewController.swift +337 -0
- package/examples/ios/SwiftUISampleApp/ContentView.swift +304 -0
- package/iOS_IMPLEMENTATION_OPTIONS.md +470 -0
- package/ios/UnifiedVideoPlayer/UnifiedVideoPlayer.podspec +33 -0
- package/jest.config.js +33 -0
- package/jitpack.yml +5 -0
- package/lerna.json +35 -0
- package/package.json +69 -0
- package/packages/PLATFORM_STATUS.md +163 -0
- package/packages/android/build.gradle +135 -0
- package/packages/android/src/main/AndroidManifest.xml +36 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/PlayerConfiguration.java +221 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/UnifiedVideoPlayer.java +1037 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/UnifiedVideoPlayer.kt +707 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/analytics/AnalyticsProvider.java +9 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/cast/CastManager.java +141 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/cast/CastOptionsProvider.java +29 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/overlay/WatermarkOverlayView.java +88 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/pip/PipActionReceiver.java +33 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/services/PlaybackService.java +110 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/services/PlayerHolder.java +19 -0
- package/packages/core/package.json +34 -0
- package/packages/core/src/BasePlayer.ts +250 -0
- package/packages/core/src/VideoPlayer.ts +237 -0
- package/packages/core/src/VideoPlayerFactory.ts +145 -0
- package/packages/core/src/index.ts +20 -0
- package/packages/core/src/interfaces/IVideoPlayer.ts +184 -0
- package/packages/core/src/interfaces.ts +240 -0
- package/packages/core/src/utils/EventEmitter.ts +66 -0
- package/packages/core/src/utils/PlatformDetector.ts +300 -0
- package/packages/core/tsconfig.json +20 -0
- package/packages/enact/package.json +51 -0
- package/packages/enact/src/VideoPlayer.js +365 -0
- package/packages/enact/src/adapters/TizenAdapter.js +354 -0
- package/packages/enact/src/index.js +82 -0
- package/packages/ios/BUILD_INSTRUCTIONS.md +108 -0
- package/packages/ios/FIX_EMBED_ISSUE.md +142 -0
- package/packages/ios/GETTING_STARTED.md +100 -0
- package/packages/ios/Package.swift +35 -0
- package/packages/ios/README.md +84 -0
- package/packages/ios/Sources/UnifiedVideoPlayer/Analytics/AnalyticsEmitter.swift +26 -0
- package/packages/ios/Sources/UnifiedVideoPlayer/DRM/FairPlayDRMManager.swift +102 -0
- package/packages/ios/Sources/UnifiedVideoPlayer/Info.plist +24 -0
- package/packages/ios/Sources/UnifiedVideoPlayer/Remote/RemoteCommandCenter.swift +109 -0
- package/packages/ios/Sources/UnifiedVideoPlayer/UnifiedVideoPlayer.swift +811 -0
- package/packages/ios/Sources/UnifiedVideoPlayer/UnifiedVideoPlayerView.swift +640 -0
- package/packages/ios/Sources/UnifiedVideoPlayer/Utilities/Color+Hex.swift +36 -0
- package/packages/ios/UnifiedVideoPlayer.podspec +27 -0
- package/packages/ios/UnifiedVideoPlayer.xcodeproj/project.pbxproj +385 -0
- package/packages/ios/build_framework.sh +55 -0
- package/packages/react-native/android/src/main/java/com/unifiedvideo/UnifiedVideoPlayerModule.kt +482 -0
- package/packages/react-native/ios/UnifiedVideoPlayer.swift +436 -0
- package/packages/react-native/package.json +51 -0
- package/packages/react-native/src/ReactNativePlayer.tsx +423 -0
- package/packages/react-native/src/VideoPlayer.tsx +224 -0
- package/packages/react-native/src/index.ts +28 -0
- package/packages/react-native/src/utils/EventEmitter.ts +66 -0
- package/packages/react-native/tsconfig.json +31 -0
- package/packages/roku/components/UnifiedVideoPlayer.brs +400 -0
- package/packages/roku/package.json +44 -0
- package/packages/roku/source/VideoPlayer.brs +231 -0
- package/packages/roku/source/main.brs +28 -0
- package/packages/web/GETTING_STARTED.md +292 -0
- package/packages/web/jest.config.js +28 -0
- package/packages/web/jest.setup.ts +110 -0
- package/packages/web/package.json +50 -0
- package/packages/web/src/SecureVideoPlayer.ts +1164 -0
- package/packages/web/src/WebPlayer.ts +3110 -0
- package/packages/web/src/__tests__/WebPlayer.test.ts +314 -0
- package/packages/web/src/index.ts +14 -0
- package/packages/web/src/paywall/PaywallController.ts +215 -0
- package/packages/web/src/react/WebPlayerView.tsx +177 -0
- package/packages/web/tsconfig.json +23 -0
- package/packages/web/webpack.config.js +45 -0
- package/server.js +131 -0
- package/server.py +84 -0
- package/test-urls.ps1 +97 -0
- package/test-video-urls.ps1 +87 -0
- package/tsconfig.json +39 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Router, type Request, type Response } from 'express';
|
|
2
|
+
import Stripe from 'stripe';
|
|
3
|
+
import { config } from '../config.js';
|
|
4
|
+
import { upsertPayment } from '../services/payments.js';
|
|
5
|
+
import { issueRentalEntitlement, expireEntitlementsForPayment } from '../services/entitlements.js';
|
|
6
|
+
import { db } from '../db.js';
|
|
7
|
+
|
|
8
|
+
export const stripeWebhookRouter = Router();
|
|
9
|
+
const stripe = new Stripe(config.stripeSecretKey, { apiVersion: '2024-06-20' });
|
|
10
|
+
|
|
11
|
+
// Stripe requires raw body at server.ts level
|
|
12
|
+
stripeWebhookRouter.post('/stripe', async (req: Request, res: Response) => {
|
|
13
|
+
const sig = req.headers['stripe-signature'];
|
|
14
|
+
if (!sig) return res.status(400).send('Missing signature');
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const event = stripe.webhooks.constructEvent(req.body, sig as string, config.stripeWebhookSecret);
|
|
18
|
+
|
|
19
|
+
if (event.type === 'checkout.session.completed') {
|
|
20
|
+
const session = event.data.object as Stripe.Checkout.Session;
|
|
21
|
+
|
|
22
|
+
const amountCents = session.amount_total ?? 0;
|
|
23
|
+
const currency = (session.currency || 'USD').toUpperCase();
|
|
24
|
+
const userId = session.metadata?.userId || '';
|
|
25
|
+
const videoId = session.metadata?.videoId || '';
|
|
26
|
+
const rentalDurationHours = Number(session.metadata?.rentalDurationHours || 48);
|
|
27
|
+
|
|
28
|
+
const payment = await upsertPayment({
|
|
29
|
+
gateway: 'stripe',
|
|
30
|
+
gatewayRef: session.id,
|
|
31
|
+
amountCents,
|
|
32
|
+
currency,
|
|
33
|
+
status: 'succeeded',
|
|
34
|
+
rawPayload: session,
|
|
35
|
+
userId,
|
|
36
|
+
videoId
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Store payment_intent as subref for refunds correlation
|
|
40
|
+
const pi = session.payment_intent ? String(session.payment_intent) : null;
|
|
41
|
+
if (pi) {
|
|
42
|
+
await db.query(
|
|
43
|
+
`UPDATE payments SET gateway_subref=$1 WHERE id=$2`,
|
|
44
|
+
[pi, payment.id]
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
await issueRentalEntitlement({
|
|
49
|
+
userId, videoId, paymentId: payment.id, rentalDurationHours
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Handle refunds: charge.refunded
|
|
54
|
+
if (event.type === 'charge.refunded') {
|
|
55
|
+
const charge = event.data.object as Stripe.Charge;
|
|
56
|
+
const pi = charge.payment_intent ? String(charge.payment_intent) : null;
|
|
57
|
+
if (pi) {
|
|
58
|
+
const { rows } = await db.query(
|
|
59
|
+
`SELECT id FROM payments WHERE gateway='stripe' AND gateway_subref=$1 LIMIT 1`,
|
|
60
|
+
[pi]
|
|
61
|
+
);
|
|
62
|
+
if (rows[0]?.id) {
|
|
63
|
+
await expireEntitlementsForPayment(rows[0].id);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
res.status(200).send('ok');
|
|
69
|
+
} catch (err) {
|
|
70
|
+
return res.status(400).send('Webhook signature verification failed');
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import bodyParser from 'body-parser';
|
|
3
|
+
import cors from 'cors';
|
|
4
|
+
import { rentalsRouter } from './routes/rentals.js';
|
|
5
|
+
import { stripeWebhookRouter } from './routes/webhooks.js';
|
|
6
|
+
// import { pesapalRouter } from './routes/pesapal.js';
|
|
7
|
+
import { cashfreeRouter } from './routes/cashfree.js';
|
|
8
|
+
import { initDb } from './db.js';
|
|
9
|
+
import { config } from './config.js';
|
|
10
|
+
|
|
11
|
+
const app = express();
|
|
12
|
+
|
|
13
|
+
// Enable CORS for browser calls from the demo
|
|
14
|
+
app.use(cors());
|
|
15
|
+
|
|
16
|
+
// Stripe webhook must be raw body
|
|
17
|
+
app.use('/api/webhooks/stripe', bodyParser.raw({ type: 'application/json' }));
|
|
18
|
+
|
|
19
|
+
// JSON body for other routes
|
|
20
|
+
app.use(bodyParser.json());
|
|
21
|
+
|
|
22
|
+
app.use('/api/rentals', rentalsRouter);
|
|
23
|
+
app.use('/api/webhooks', stripeWebhookRouter);
|
|
24
|
+
app.use('/api/rentals', cashfreeRouter);
|
|
25
|
+
|
|
26
|
+
const PORT = process.env.PORT ? Number(process.env.PORT) : 3000;
|
|
27
|
+
|
|
28
|
+
(async () => {
|
|
29
|
+
try {
|
|
30
|
+
if (process.env.DISABLE_DB === '1' || !config.dbUrl) {
|
|
31
|
+
console.warn('[rental-api] DB disabled or no DATABASE_URL set - starting without database. Some endpoints may be unavailable.');
|
|
32
|
+
} else {
|
|
33
|
+
await initDb();
|
|
34
|
+
}
|
|
35
|
+
app.listen(PORT, () => console.log(`[rental-api] listening on :${PORT}`));
|
|
36
|
+
} catch (err) {
|
|
37
|
+
console.error('Failed to start server', err);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
})();
|
|
41
|
+
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { db } from '../db.js';
|
|
2
|
+
|
|
3
|
+
export async function getEntitlement(userId: string, videoId: string) {
|
|
4
|
+
const { rows } = await db.query(
|
|
5
|
+
`SELECT * FROM entitlements
|
|
6
|
+
WHERE user_id=$1 AND video_id=$2
|
|
7
|
+
ORDER BY created_at DESC LIMIT 1`,
|
|
8
|
+
[userId, videoId]
|
|
9
|
+
);
|
|
10
|
+
if (!rows[0]) return { entitled: false as const };
|
|
11
|
+
const e = rows[0];
|
|
12
|
+
const now = new Date();
|
|
13
|
+
const entitled = new Date(e.expires_at) > now && e.status === 'active';
|
|
14
|
+
return { entitled, expiresAt: e.expires_at };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function issueRentalEntitlement(args: {
|
|
18
|
+
userId: string; videoId: string; paymentId: string; rentalDurationHours: number;
|
|
19
|
+
}) {
|
|
20
|
+
const startsAt = new Date();
|
|
21
|
+
const expiresAt = new Date(startsAt.getTime() + args.rentalDurationHours * 3600 * 1000);
|
|
22
|
+
|
|
23
|
+
await db.query(
|
|
24
|
+
`UPDATE entitlements SET status='expired'
|
|
25
|
+
WHERE user_id=$1 AND video_id=$2 AND status='active' AND expires_at <= NOW()`,
|
|
26
|
+
[args.userId, args.videoId]
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const { rows } = await db.query(
|
|
30
|
+
`INSERT INTO entitlements (user_id, video_id, type, starts_at, expires_at, status, source_payment_id)
|
|
31
|
+
VALUES ($1,$2,'rental',$3,$4,'active',$5)
|
|
32
|
+
RETURNING *`,
|
|
33
|
+
[args.userId, args.videoId, startsAt.toISOString(), expiresAt.toISOString(), args.paymentId]
|
|
34
|
+
);
|
|
35
|
+
return rows[0];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function expireEntitlementsForPayment(paymentId: string) {
|
|
39
|
+
await db.query(
|
|
40
|
+
`UPDATE entitlements SET status='expired', expires_at=LEAST(expires_at, NOW())
|
|
41
|
+
WHERE source_payment_id=$1 AND status='active'`,
|
|
42
|
+
[paymentId]
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { db } from '../db.js';
|
|
2
|
+
|
|
3
|
+
export async function upsertPayment(args: {
|
|
4
|
+
gateway: 'stripe'|'pesapal'|'google_pay'|'cashfree',
|
|
5
|
+
gatewayRef: string,
|
|
6
|
+
amountCents: number,
|
|
7
|
+
currency: string,
|
|
8
|
+
status: 'succeeded'|'pending'|'failed'|'refunded',
|
|
9
|
+
rawPayload: any,
|
|
10
|
+
userId: string,
|
|
11
|
+
videoId: string
|
|
12
|
+
}) {
|
|
13
|
+
const { rows } = await db.query(
|
|
14
|
+
`INSERT INTO payments (gateway,gateway_ref,amount_cents,currency,status,raw_payload,user_id,video_id)
|
|
15
|
+
VALUES ($1,$2,$3,$4,$5,$6,$7,$8)
|
|
16
|
+
ON CONFLICT (gateway_ref) DO UPDATE SET status=EXCLUDED.status, raw_payload=EXCLUDED.raw_payload
|
|
17
|
+
RETURNING *`,
|
|
18
|
+
[args.gateway, args.gatewayRef, args.amountCents, args.currency, args.status, args.rawPayload, args.userId, args.videoId]
|
|
19
|
+
);
|
|
20
|
+
return rows[0];
|
|
21
|
+
}
|
|
22
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "ES2020",
|
|
5
|
+
"moduleResolution": "Node",
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
"rootDir": "src",
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"strict": true,
|
|
12
|
+
"resolveJsonModule": true
|
|
13
|
+
},
|
|
14
|
+
"include": ["src/**/*"],
|
|
15
|
+
"exclude": ["node_modules", "dist"]
|
|
16
|
+
}
|
|
17
|
+
|
package/check-urls.ps1
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Test video URLs in the framework
|
|
2
|
+
|
|
3
|
+
Write-Host ""
|
|
4
|
+
Write-Host "Testing Video URLs in Unified Video Framework" -ForegroundColor Cyan
|
|
5
|
+
Write-Host "============================================================" -ForegroundColor Cyan
|
|
6
|
+
|
|
7
|
+
$urls = @(
|
|
8
|
+
@{Name="Mux HLS Stream"; URL="https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8"; Type="HLS"},
|
|
9
|
+
@{Name="Unified Streaming HLS"; URL="https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel.ism/.m3u8"; Type="HLS"},
|
|
10
|
+
@{Name="Akamai DASH BBB"; URL="https://dash.akamaized.net/akamai/bbb_30fps/bbb_30fps.mpd"; Type="DASH"},
|
|
11
|
+
@{Name="Akamai DASH Envivio"; URL="https://dash.akamaized.net/envivio/EnvivioDash3/manifest.mpd"; Type="DASH"},
|
|
12
|
+
@{Name="Google Shaka DASH"; URL="https://storage.googleapis.com/shaka-demo-assets/angel-one/dash.mpd"; Type="DASH"},
|
|
13
|
+
@{Name="Big Buck Bunny MP4"; URL="https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"; Type="MP4"},
|
|
14
|
+
@{Name="Elephants Dream MP4"; URL="https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4"; Type="MP4"},
|
|
15
|
+
@{Name="Sintel MP4"; URL="https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/Sintel.mp4"; Type="MP4"}
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
$workingCount = 0
|
|
19
|
+
$brokenCount = 0
|
|
20
|
+
$uncertainCount = 0
|
|
21
|
+
|
|
22
|
+
foreach ($item in $urls) {
|
|
23
|
+
Write-Host ""
|
|
24
|
+
Write-Host "Testing: $($item.Name) [$($item.Type)]" -ForegroundColor Yellow
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
$response = Invoke-WebRequest -Uri $item.URL -Method Head -TimeoutSec 5 -UseBasicParsing -ErrorAction Stop
|
|
28
|
+
Write-Host "[OK] URL is accessible" -ForegroundColor Green
|
|
29
|
+
$workingCount++
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
$errorMsg = $_.Exception.Message
|
|
33
|
+
|
|
34
|
+
if ($errorMsg -match "403") {
|
|
35
|
+
Write-Host "[ERROR] 403 Forbidden - Access denied" -ForegroundColor Red
|
|
36
|
+
$brokenCount++
|
|
37
|
+
}
|
|
38
|
+
elseif ($errorMsg -match "404") {
|
|
39
|
+
Write-Host "[ERROR] 404 Not Found" -ForegroundColor Red
|
|
40
|
+
$brokenCount++
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
# Try GET request with byte range
|
|
44
|
+
try {
|
|
45
|
+
$response2 = Invoke-WebRequest -Uri $item.URL -Method Get -Headers @{"Range"="bytes=0-1"} -TimeoutSec 5 -UseBasicParsing -ErrorAction Stop
|
|
46
|
+
Write-Host "[OK] URL is accessible (via GET)" -ForegroundColor Green
|
|
47
|
+
$workingCount++
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
Write-Host "[WARNING] Could not verify - may still work in browser" -ForegroundColor Yellow
|
|
51
|
+
$uncertainCount++
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
Write-Host ""
|
|
58
|
+
Write-Host "============================================================" -ForegroundColor Cyan
|
|
59
|
+
Write-Host "SUMMARY" -ForegroundColor Cyan
|
|
60
|
+
Write-Host "============================================================" -ForegroundColor Cyan
|
|
61
|
+
Write-Host ""
|
|
62
|
+
Write-Host "Total URLs tested: $($urls.Count)"
|
|
63
|
+
Write-Host "Working: $workingCount" -ForegroundColor Green
|
|
64
|
+
Write-Host "Broken: $brokenCount" -ForegroundColor Red
|
|
65
|
+
Write-Host "Uncertain: $uncertainCount" -ForegroundColor Yellow
|
|
66
|
+
|
|
67
|
+
if ($brokenCount -gt 0) {
|
|
68
|
+
Write-Host ""
|
|
69
|
+
Write-Host "ACTION REQUIRED: Found broken URLs that need replacement" -ForegroundColor Red
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
Write-Host ""
|
|
73
|
+
Write-Host "All URLs are working or accessible!" -ForegroundColor Green
|
|
74
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# Comparison Report: Enhanced-Player.html vs WebPlayer.ts
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
This document compares the `enhanced-player.html` standalone implementation with the `WebPlayer.ts` class implementation to ensure feature parity.
|
|
5
|
+
|
|
6
|
+
## β
MATCHING FEATURES
|
|
7
|
+
|
|
8
|
+
### 1. **UI Structure**
|
|
9
|
+
Both implementations have identical UI components:
|
|
10
|
+
- Player wrapper with gradient backgrounds
|
|
11
|
+
- Video container with aspect ratio 16:9
|
|
12
|
+
- Watermark canvas overlay
|
|
13
|
+
- Top and bottom gradient overlays
|
|
14
|
+
- Loading spinner
|
|
15
|
+
- Center play button
|
|
16
|
+
- Controls bar with all buttons
|
|
17
|
+
|
|
18
|
+
### 2. **CSS Styles**
|
|
19
|
+
Both use the same styling:
|
|
20
|
+
- Identical gradient colors (#ff0080, #8b00ff)
|
|
21
|
+
- Same backdrop-filter blur effects
|
|
22
|
+
- Matching button sizes and animations
|
|
23
|
+
- Same hover effects and transitions
|
|
24
|
+
- Identical opacity and transform values
|
|
25
|
+
|
|
26
|
+
### 3. **Control Elements**
|
|
27
|
+
Both have the same controls:
|
|
28
|
+
- Play/pause button (center and in controls)
|
|
29
|
+
- Skip back/forward buttons (10 seconds)
|
|
30
|
+
- Volume control with hover panel
|
|
31
|
+
- Time display (current/duration)
|
|
32
|
+
- Progress bar with buffered indicator
|
|
33
|
+
- Settings button
|
|
34
|
+
- Fullscreen button
|
|
35
|
+
- All use same SVG icons
|
|
36
|
+
|
|
37
|
+
### 4. **Auto-Hide Controls**
|
|
38
|
+
Both implement identical behavior:
|
|
39
|
+
- Show controls on mouse movement
|
|
40
|
+
- Hide after 3 seconds of inactivity when playing
|
|
41
|
+
- Immediate hide on mouse leave when playing
|
|
42
|
+
- Keep visible when hovering over controls
|
|
43
|
+
- Cursor hiding with controls
|
|
44
|
+
|
|
45
|
+
### 5. **Volume Control**
|
|
46
|
+
Both have matching implementation:
|
|
47
|
+
- Volume button with mute toggle
|
|
48
|
+
- Hover panel that appears with delay
|
|
49
|
+
- Draggable volume slider
|
|
50
|
+
- Volume percentage display
|
|
51
|
+
- Smart panel persistence on hover/drag
|
|
52
|
+
|
|
53
|
+
### 6. **Progress Bar**
|
|
54
|
+
Both feature:
|
|
55
|
+
- Click to seek
|
|
56
|
+
- Drag to scrub
|
|
57
|
+
- Buffered progress display
|
|
58
|
+
- Progress handle that appears on hover
|
|
59
|
+
- Percentage-based positioning
|
|
60
|
+
|
|
61
|
+
### 7. **Watermark System**
|
|
62
|
+
Both implement:
|
|
63
|
+
- Canvas-based watermark
|
|
64
|
+
- Gradient text effect
|
|
65
|
+
- Random positioning every 5 seconds
|
|
66
|
+
- Same "PREMIUM" text with timestamp
|
|
67
|
+
|
|
68
|
+
### 8. **Event Handling**
|
|
69
|
+
Both handle:
|
|
70
|
+
- Context menu disabled
|
|
71
|
+
- Play/pause events
|
|
72
|
+
- Time updates
|
|
73
|
+
- Volume changes
|
|
74
|
+
- Loading states
|
|
75
|
+
- Fullscreen changes
|
|
76
|
+
|
|
77
|
+
## β οΈ DIFFERENCES
|
|
78
|
+
|
|
79
|
+
### 1. **Additional Features in enhanced-player.html**
|
|
80
|
+
The HTML version includes extra features NOT in WebPlayer.ts:
|
|
81
|
+
|
|
82
|
+
#### Top Bar Elements:
|
|
83
|
+
- **Title Bar** - Video title and subtitle display
|
|
84
|
+
- **Cast Button** - Chromecast support
|
|
85
|
+
- **Playlist Button** - Add to playlist functionality
|
|
86
|
+
- **Share Button** - Share video functionality
|
|
87
|
+
- **Quality Badge** - Visual quality indicator
|
|
88
|
+
|
|
89
|
+
#### Advanced Features:
|
|
90
|
+
- **Keyboard Shortcuts** - Full keyboard control (Space, arrows, numbers, etc.)
|
|
91
|
+
- **Shortcut Indicator** - Visual feedback for keyboard actions
|
|
92
|
+
- **Time Tooltip** - Hover preview on progress bar
|
|
93
|
+
- **Settings Menu** - Playback speed and quality options
|
|
94
|
+
- **Picture-in-Picture Button** - PiP mode support
|
|
95
|
+
|
|
96
|
+
### 2. **Implementation Differences**
|
|
97
|
+
|
|
98
|
+
| Feature | enhanced-player.html | WebPlayer.ts |
|
|
99
|
+
|---------|---------------------|--------------|
|
|
100
|
+
| Architecture | Standalone class | Extends BasePlayer |
|
|
101
|
+
| Video Loading | Direct URL | HLS/DASH support |
|
|
102
|
+
| Quality Control | Manual settings | Adaptive bitrate |
|
|
103
|
+
| Subtitles | Not implemented | Full support |
|
|
104
|
+
| Error Handling | Basic | Comprehensive |
|
|
105
|
+
| Events | Internal only | Event emitter system |
|
|
106
|
+
|
|
107
|
+
### 3. **Missing in WebPlayer.ts**
|
|
108
|
+
To achieve complete parity, WebPlayer.ts needs:
|
|
109
|
+
|
|
110
|
+
1. **Top Controls Section**
|
|
111
|
+
- Title bar with video metadata
|
|
112
|
+
- Cast, playlist, share buttons
|
|
113
|
+
- Quality badge display
|
|
114
|
+
|
|
115
|
+
2. **Keyboard Shortcuts**
|
|
116
|
+
- Space/K for play/pause
|
|
117
|
+
- Arrow keys for seek/volume
|
|
118
|
+
- Number keys for percentage seek
|
|
119
|
+
- M for mute, F for fullscreen, P for PiP
|
|
120
|
+
|
|
121
|
+
3. **Visual Feedback**
|
|
122
|
+
- Shortcut indicator overlay
|
|
123
|
+
- Time tooltip on progress hover
|
|
124
|
+
- Settings menu with speed/quality options
|
|
125
|
+
|
|
126
|
+
4. **PiP Button**
|
|
127
|
+
- Picture-in-Picture toggle in controls
|
|
128
|
+
|
|
129
|
+
5. **Advanced Volume Features**
|
|
130
|
+
- Keyboard volume control
|
|
131
|
+
- More refined hover timing
|
|
132
|
+
|
|
133
|
+
## π Feature Comparison Table
|
|
134
|
+
|
|
135
|
+
| Feature | enhanced-player.html | WebPlayer.ts | Status |
|
|
136
|
+
|---------|---------------------|--------------|--------|
|
|
137
|
+
| Custom Controls | β
| β
| β
Matching |
|
|
138
|
+
| Auto-hide Controls | β
| β
| β
Matching |
|
|
139
|
+
| Volume Panel | β
| β
| β
Matching |
|
|
140
|
+
| Progress Bar | β
| β
| β
Matching |
|
|
141
|
+
| Watermark | β
| β
| β
Matching |
|
|
142
|
+
| Loading Spinner | β
| β
| β
Matching |
|
|
143
|
+
| Fullscreen | β
| β
| β
Matching |
|
|
144
|
+
| Context Menu Disabled | β
| β
| β
Matching |
|
|
145
|
+
| Keyboard Shortcuts | β
| β | β οΈ Missing |
|
|
146
|
+
| Time Tooltip | β
| β | β οΈ Missing |
|
|
147
|
+
| Settings Menu | β
| β | β οΈ Missing |
|
|
148
|
+
| PiP Support | β
| β
* | β οΈ No UI button |
|
|
149
|
+
| Title Bar | β
| β | β οΈ Missing |
|
|
150
|
+
| Cast Support | β
| β | β οΈ Missing |
|
|
151
|
+
| Share Feature | β
| β | β οΈ Missing |
|
|
152
|
+
| Quality Badge | β
| β | β οΈ Missing |
|
|
153
|
+
| Shortcut Indicator | β
| β | β οΈ Missing |
|
|
154
|
+
| HLS/DASH Support | β | β
| π― WebPlayer only |
|
|
155
|
+
| Subtitle Support | β | β
| π― WebPlayer only |
|
|
156
|
+
| Event System | β | β
| π― WebPlayer only |
|
|
157
|
+
|
|
158
|
+
*PiP methods exist but no UI button
|
|
159
|
+
|
|
160
|
+
## π§ Recommendations
|
|
161
|
+
|
|
162
|
+
To make WebPlayer.ts match enhanced-player.html completely:
|
|
163
|
+
|
|
164
|
+
1. **Add keyboard event listener** in `setupControlsEventListeners()`
|
|
165
|
+
2. **Create settings menu** with speed and quality options
|
|
166
|
+
3. **Add time tooltip** to progress bar hover
|
|
167
|
+
4. **Include PiP button** in controls bar
|
|
168
|
+
5. **Add title bar** section with metadata display
|
|
169
|
+
6. **Implement cast support** (optional, browser-specific)
|
|
170
|
+
7. **Add share functionality** using Web Share API
|
|
171
|
+
8. **Include quality badge** in controls
|
|
172
|
+
9. **Create shortcut indicator** overlay
|
|
173
|
+
|
|
174
|
+
## Conclusion
|
|
175
|
+
|
|
176
|
+
The core functionality and visual design are **mostly matching** between both implementations. WebPlayer.ts has the essential premium UI features but lacks some of the advanced interactive features present in enhanced-player.html. The main differences are:
|
|
177
|
+
|
|
178
|
+
- WebPlayer.ts focuses on **video format compatibility** (HLS/DASH)
|
|
179
|
+
- enhanced-player.html focuses on **user interaction features** (keyboard, sharing, etc.)
|
|
180
|
+
|
|
181
|
+
Both serve their purposes well, with WebPlayer.ts being more suitable for production use with streaming protocols, while enhanced-player.html provides a richer interactive experience for demonstration purposes.
|
package/docs/PAYWALL.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Paywall Integration (Stripe + Cashfree)
|
|
2
|
+
|
|
3
|
+
This document describes how to enable a dynamic paywall for rental flows across the Web player. It covers free preview gating, in-player paywall overlay (80% modal), popup checkout windows, entitlement polling/verification, and gateway configuration.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
- Free preview limit (freeDuration) enforced locally and during casting
|
|
7
|
+
- 80% in-player overlay on preview end showing metadata and a Rent Now CTA
|
|
8
|
+
- Gateway selection list rendered dynamically from configuration
|
|
9
|
+
- Popup checkout window (not a tab) for Stripe and Cashfree
|
|
10
|
+
- Auto-close popup and resume playback on payment success
|
|
11
|
+
- Cancel handling: popup closes and overlay returns to gateway list
|
|
12
|
+
|
|
13
|
+
## Backend Requirements
|
|
14
|
+
- Rental API exposes:
|
|
15
|
+
- Stripe: `POST /api/rentals/stripe/checkout-session` (returns { url })
|
|
16
|
+
- Cashfree: `POST /api/rentals/cashfree/order` (returns { paymentLink, orderId })
|
|
17
|
+
- Cashfree verify: `GET /api/rentals/cashfree/verify?orderId=&userId=&videoId=`
|
|
18
|
+
- Environment variables:
|
|
19
|
+
- STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET
|
|
20
|
+
- CASHFREE_APP_ID, CASHFREE_SECRET_KEY, CASHFREE_BASE_URL (sandbox/live)
|
|
21
|
+
|
|
22
|
+
## Core Types
|
|
23
|
+
```
|
|
24
|
+
export interface PaywallConfig {
|
|
25
|
+
enabled: boolean;
|
|
26
|
+
apiBase: string;
|
|
27
|
+
userId: string;
|
|
28
|
+
videoId: string;
|
|
29
|
+
gateways: Array<'stripe' | 'cashfree'>;
|
|
30
|
+
branding?: { title?: string; description?: string; logoUrl?: string; theme?: any };
|
|
31
|
+
popup?: { width?: number; height?: number };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface PlayerConfig {
|
|
35
|
+
...
|
|
36
|
+
freeDuration?: number;
|
|
37
|
+
paywall?: PaywallConfig;
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Web Player Usage
|
|
42
|
+
```
|
|
43
|
+
import { WebPlayerView } from '@unified-video/web/react';
|
|
44
|
+
|
|
45
|
+
<WebPlayerView
|
|
46
|
+
url="https://.../movie.m3u8"
|
|
47
|
+
type="hls"
|
|
48
|
+
freeDuration={60}
|
|
49
|
+
paywall={{
|
|
50
|
+
enabled: true,
|
|
51
|
+
apiBase: 'http://localhost:3100',
|
|
52
|
+
userId: 'u1',
|
|
53
|
+
videoId: 'v1',
|
|
54
|
+
gateways: ['stripe','cashfree'],
|
|
55
|
+
branding: { title: 'Continue watching', description: 'Rent to continue watching this video.' },
|
|
56
|
+
popup: { width: 1000, height: 800 }
|
|
57
|
+
}}
|
|
58
|
+
/>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Or fetch paywall config dynamically per tenant:
|
|
62
|
+
```
|
|
63
|
+
<WebPlayerView
|
|
64
|
+
url="..."
|
|
65
|
+
freeDuration={60}
|
|
66
|
+
paywallConfigUrl="/api/rentals/config?tenant=acme"
|
|
67
|
+
/>
|
|
68
|
+
```
|
|
69
|
+
`paywallConfigUrl` should return PaywallConfig JSON.
|
|
70
|
+
|
|
71
|
+
## Stripe
|
|
72
|
+
- We open the exact Checkout URL returned from `stripe/checkout-session`.
|
|
73
|
+
- successUrl: `.../enhanced-player.html?rental=success&popup=1`
|
|
74
|
+
- cancelUrl: `.../enhanced-player.html?rental=cancel&popup=1`
|
|
75
|
+
- The popup page posts `{ type:'uvfCheckout', status:'success|cancel' }` to the opener and closes.
|
|
76
|
+
- Entitlement is granted by webhook; the player polls entitlement and resumes on success.
|
|
77
|
+
|
|
78
|
+
## Cashfree
|
|
79
|
+
- We call `cashfree/order` to get `paymentLink` and `orderId`.
|
|
80
|
+
- Open `paymentLink` in a popup; on success the return URL includes `?rental=success&popup=1&order_id=...`.
|
|
81
|
+
- The popup posts a message with `{ type:'uvfCheckout', status:'success', orderId }` and closes.
|
|
82
|
+
- The player verifies via `cashfree/verify` and grants entitlement immediately.
|
|
83
|
+
|
|
84
|
+
## Styling & Branding
|
|
85
|
+
- The overlay uses minimal inline styles for portability. You can pass `branding` and `popup` in `PaywallConfig` for per-tenant customization.
|
|
86
|
+
|
|
87
|
+
## Removing Pesapal
|
|
88
|
+
- Pesapal code is disabled and not wired in the server.
|
|
89
|
+
- Demo and shared paywall UI list Stripe and Cashfree only.
|
|
90
|
+
|
|
91
|
+
## Troubleshooting
|
|
92
|
+
- Popup blocked: allow popups for your domain (localhost in dev).
|
|
93
|
+
- Stripe βSomething went wrongβ: never append query params to the Checkout URL; only add `popup=1` to your own return/cancel URLs.
|
|
94
|
+
- Entitlement doesnβt resume: ensure webhooks (Stripe) or `cashfree/verify` are reachable; check Rental API logs.
|
|
95
|
+
|