npaw_forge 9.7.5
npaw_forge: ^9.7.5 copied to clipboard
NPAW Forge analytics for Flutter: the shared native Forge Rust core over Dart FFI on Android/iOS, the Forge Web runtime on Flutter Web, and typed video, ad, and session reporting.
example/lib/main.dart
// Copyright (C) NPAW - All Rights Reserved
//
// This source code is protected under international copyright law. All rights
// reserved and protected by the copyright holders. This file is confidential and
// only available to authorized individuals with the permission of the copyright
// holders. If you encounter this file and do not have permission, please contact
// the copyright holders and delete this file.
//
//
// @author: Victor Soto <victor.soto@nicepeopleatwork.com>
// @description: NPAW Forge Flutter example app playing a sample stream with automatic analytics.
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:npaw_forge/npaw_forge.dart';
import 'package:npaw_forge_video_player/npaw_forge_video_player.dart';
import 'package:video_player/video_player.dart';
import 'two_player_screen.dart';
/// Demo account; override with --dart-define=FORGE_ACCOUNT_CODE=yourAccount.
const String accountCode = String.fromEnvironment(
'FORGE_ACCOUNT_CODE',
defaultValue: 'npawdemodev',
);
/// Public Big Buck Bunny MP4 from the ExoPlayer test-media bucket. The former
/// `gtv-videos-bucket` sample answers HTTP 403 (AccessDenied) since 2026-08.
const String sampleStream = String.fromEnvironment(
'FORGE_SAMPLE_STREAM',
defaultValue:
'https://storage.googleapis.com/exoplayer-test-media-0/BigBuckBunny_320x180.mp4',
);
/// Ingest endpoint; point smoke runs at stage, never production.
const String lmaEndpoint = String.fromEnvironment('FORGE_LMA_ENDPOINT');
/// Opt-in iOS access-log QoS smoke toggle
/// (--dart-define=FORGE_IOS_ACCESS_LOG=true).
const bool enableAccessLogQos =
bool.fromEnvironment('FORGE_IOS_ACCESS_LOG', defaultValue: false);
/// Opt-in diagnostics printer for smoke evidence
/// (--dart-define=FORGE_DEBUG_DIAGNOSTICS=true).
const bool debugDiagnostics =
bool.fromEnvironment('FORGE_DEBUG_DIAGNOSTICS', defaultValue: false);
/// Opt-in scripted lifecycle for stage smokes
/// (--dart-define=FORGE_SMOKE_SCRIPT=true): once playback starts the screen
/// pauses, resumes, seeks forward and back, performs one successful and one
/// failing HTTP request as network observations, seeks into unbuffered
/// content (a window for provoking a buffer with network throttling), and
/// finally closes the Forge view. Every step logs a `forge smoke:` marker so
/// the captured log shows what was driven and when.
const bool smokeScript =
bool.fromEnvironment('FORGE_SMOKE_SCRIPT', defaultValue: false);
/// Opt-in two-player attribution page for the iOS access-log feed
/// (--dart-define=FORGE_TWO_PLAYER=true): two `video_player` sessions on
/// different HLS streams play at once; see `two_player_screen.dart`.
const bool twoPlayer =
bool.fromEnvironment('FORGE_TWO_PLAYER', defaultValue: false);
/// Small CORS-enabled resources for the smoke network observations.
const String smokeNetworkOkUrl =
'https://flutter.github.io/assets-for-api-docs/assets/widgets/owl.jpg';
const String smokeNetworkMissingUrl =
'https://flutter.github.io/assets-for-api-docs/assets/widgets/forge-smoke-missing.jpg';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
final forge = await NpawForge.start(
ForgeConfig(
accountCode: accountCode,
appName: 'npaw-forge-flutter-example',
lmaEndpoint: lmaEndpoint.isEmpty ? null : lmaEndpoint,
logLevel: 'info',
),
);
if (enableAccessLogQos) {
final active = forge.enableIosAccessLogQos();
debugPrint('forge iOS access-log QoS feed active: $active');
}
if (debugDiagnostics) {
forge.events.listen((event) => debugPrint('forge event: ${event.raw}'));
}
WidgetsBinding.instance.addObserver(ForgeLifecycleObserver(forge));
runApp(ForgeExampleApp(forge: forge));
}
class ForgeExampleApp extends StatelessWidget {
const ForgeExampleApp({required this.forge, super.key});
final NpawForge forge;
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'NPAW Forge Example',
navigatorObservers: [ForgeRouteObserver(forge)],
theme: ThemeData(colorSchemeSeed: const Color(0xFF2E5BFF)),
home: twoPlayer
? TwoPlayerScreen(forge: forge)
: PlayerScreen(forge: forge),
);
}
}
class PlayerScreen extends StatefulWidget {
const PlayerScreen({required this.forge, super.key});
final NpawForge forge;
@override
State<PlayerScreen> createState() => _PlayerScreenState();
}
class _PlayerScreenState extends State<PlayerScreen> {
late final VideoPlayerController _controller;
ForgeVideoPlayerAttachment? _attachment;
Timer? _qosTimer;
String _status = 'initializing';
@override
void initState() {
super.initState();
_controller = VideoPlayerController.networkUrl(Uri.parse(sampleStream));
_setUp();
}
Future<void> _setUp() async {
_attachment = await attachVideoPlayer(
widget.forge,
_controller,
metadata: const ContentMetadata(
title: 'Big Buck Bunny',
contentId: 'bbb-mp4',
isLive: false,
),
);
await _controller.initialize();
if (kIsWeb) {
// Browsers block unmuted programmatic autoplay (Chrome's autoplay
// policy) and video_player then reports a fatal PLAYER_ERROR; a muted
// start is always allowed. Real apps start playback from a tap.
await _controller.setVolume(0);
}
await _controller.play();
if (debugDiagnostics) {
_qosTimer = Timer.periodic(const Duration(seconds: 5), (_) {
final json = _attachment?.adapter.metadata().toJson();
debugPrint('forge qos sample: bitrate=${json?['bitrate']} '
'droppedFrames=${json?['droppedFrames']} qos=${json?['qos']}');
});
}
if (smokeScript) {
unawaited(_runSmokeScript());
}
if (mounted) {
setState(() {
_status = 'playing (forge ${widget.forge.nativeVersion ?? 'web'})';
});
}
}
/// One deterministic lifecycle per run; see [smokeScript].
Future<void> _runSmokeScript() async {
Future<void> step(
String label,
Duration after,
Future<void> Function() action,
) async {
await Future<void>.delayed(after);
if (!mounted) {
return;
}
debugPrint('forge smoke: $label');
await action();
}
await step('pause', const Duration(seconds: 15), _controller.pause);
await step('resume', const Duration(seconds: 5), _controller.play);
await step(
'seek +30s',
const Duration(seconds: 6),
() => _seekBy(const Duration(seconds: 30)),
);
await step(
'seek -10s',
const Duration(seconds: 8),
() => _seekBy(const Duration(seconds: -10)),
);
await step(
'network observations',
const Duration(seconds: 6),
_reportSmokeNetworkObservations,
);
await step(
'seek +120s (buffer window: throttle the network now)',
const Duration(seconds: 6),
() => _seekBy(const Duration(seconds: 120)),
);
await step('stop', const Duration(seconds: 45), () async {
// The same teardown path as dispose(): detach analytics, close the view.
await _attachment?.dispose();
_attachment = null;
await _controller.pause();
});
if (mounted) {
setState(() => _status = 'smoke complete');
}
debugPrint('forge smoke: done');
}
/// One successful and one failing GET over package:http. Android/iOS report
/// them through [NpawForge.reportNetworkEvent]; on Flutter Web the Forge
/// Web runtime's automatic fetch/XHR capture already observes them, so no
/// manual report is made there.
Future<void> _reportSmokeNetworkObservations() async {
for (final url in const [smokeNetworkOkUrl, smokeNetworkMissingUrl]) {
final stopwatch = Stopwatch()..start();
int? statusCode;
int? responseBytes;
String? errorMessage;
try {
final response = await http.get(Uri.parse(url));
statusCode = response.statusCode;
responseBytes = response.bodyBytes.length;
} catch (error) {
errorMessage = '$error';
}
stopwatch.stop();
debugPrint('forge smoke: GET $url -> ${statusCode ?? errorMessage} '
'in ${stopwatch.elapsedMilliseconds} ms');
if (kIsWeb) {
continue;
}
final outcome = await widget.forge.reportNetworkEvent(
NetworkObservation(
eventId: 'smoke-${DateTime.now().microsecondsSinceEpoch}',
targetUrl: url,
method: 'GET',
source: 'flutter-example-smoke',
durationMs: stopwatch.elapsedMilliseconds,
responseStatusCode: statusCode ?? 0,
outcome: errorMessage != null
? 'network_error'
: (statusCode! >= 400 ? 'http_error' : 'success'),
errorMessage: errorMessage,
responseBytes: responseBytes,
),
);
debugPrint('forge smoke: network observation -> $outcome');
}
}
@override
void dispose() {
_qosTimer?.cancel();
// Detach analytics before the controller goes away.
_attachment?.dispose();
_controller.dispose();
super.dispose();
}
Future<void> _seekBy(Duration delta) async {
final position = await _controller.position;
if (position != null) {
await _controller.seekTo(position + delta);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('NPAW Forge Example')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
AspectRatio(
aspectRatio: _controller.value.isInitialized
? _controller.value.aspectRatio
: 16 / 9,
child: VideoPlayer(_controller),
),
const SizedBox(height: 12),
Text(_status),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: const Icon(Icons.play_arrow),
onPressed: () => _controller.play(),
),
IconButton(
icon: const Icon(Icons.pause),
onPressed: () => _controller.pause(),
),
IconButton(
icon: const Icon(Icons.replay_10),
onPressed: () => _seekBy(const Duration(seconds: -10)),
),
IconButton(
icon: const Icon(Icons.forward_10),
onPressed: () => _seekBy(const Duration(seconds: 10)),
),
],
),
],
),
),
);
}
}