npaw_forge 10.3.0 copy "npaw_forge: ^10.3.0" to clipboard
npaw_forge: ^10.3.0 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)),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}
0
likes
150
points
707
downloads

Documentation

API reference

Publisher

verified publishernicepeopleatwork.com

Weekly Downloads

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.

Homepage
Repository

License

unknown (license)

Dependencies

ffi, flutter, flutter_web_plugins, web

More

Packages that depend on npaw_forge

Packages that implement npaw_forge