GardenMobile & interfaces

An older Flutter RTMP publishing and HLS playback experiment

Separate stream publication, playlist delivery, player initialization, and controls.

RTMP publication and HLS playback serve different sides of a live-streaming system. The earlier experiment used a publishing library for the sender and a player around an HLS .m3u8 playlist for viewers.

Its comparison of player packages reflected the versions tested then. Those outcomes are not current compatibility recommendations, and a playlist’s file suffix alone does not establish whether the player can follow a live update.

The earlier Android setup

*build.gradle*

```
    aaptOptions {
        noCompress '.flv', '.mp4'
    }

    signingConfigs {}
```

*AndroidManifest.xml*

```
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.INTERNET"/>
    <application
        android:requestLegacyExternalStorage="true"
        >
```

*pubspec.yaml*

```
  fijkplayer: ^0.8.7
  permission_handler: ^5.1.0+2
```

*request storage permission*

```
Permission.storage.request();
```

Player and control panel

Read the historical playback widgets

import 'package:fijkplayer/fijkplayer.dart';
import 'package:flutter/material.dart';
import 'package:nft/services/safety/base_stateful.dart';
import 'package:nft/utils/app_extension.dart';
import 'package:nft/utils/app_style.dart';

/*
/// PARENT'S WIDGET
/// Controller, state
  final WM3u8Controller wm3u8controller = WM3u8Controller();
  WM3u8State wm3u8state;

  @override
  Future<void> afterFirstBuild(BuildContext context) async {
    super.afterFirstBuild(context);
    wm3u8controller.onStateChange = (WM3u8State state) {
      print('onStateChange: $state');
      setState(() {
        wm3u8state = state;
      });
    };
  }

/// BUILD
/// Stream container
WM3u8Container(
  streamUrl: streamUrl,
  controller: wm3u8controller,
),

/// Refesh button
if (wm3u8state == WM3u8State.error ||
    wm3u8state == WM3u8State.completed)
  Container(
    alignment: Alignment.center,
    margin: EdgeInsets.only(top: 95.H),
    child: IconButton(
        icon: const Icon(
          Icons.refresh,
          color: Colors.white,
        ),
        onPressed: () {
          wm3u8controller.onRestart();
        }),
  )

 */

enum WM3u8State { loading, started, paused, error, completed }

class WM3u8Controller {
  WM3u8Controller();

  // Implement in parent's widget
  Function(WM3u8State value) onStateChange = (_) {};

  // Implement in widget
  Function() onRestart;
  FijkPlayer player;
}

class WM3u8Container extends StatefulWidget {
  const WM3u8Container({Key key, this.controller, this.streamUrl})
      : super(key: key);

  final String streamUrl;
  final WM3u8Controller controller;

  @override
  _WM3u8ContainerState createState() => _WM3u8ContainerState();
}

class _WM3u8ContainerState extends BaseStateful<WM3u8Container> {
  WM3u8Controller get controller => widget.controller;

  WM3u8State _state;

  set state(WM3u8State value) {
    controller.onStateChange(value);
    setState(() {
      _state = value;
    });
  }

  @override
  void initState() {
    super.initState();
    controller.onRestart = () {
      state = WM3u8State.loading;
    };
  }

  @override
  void afterFirstBuild(BuildContext context) {
    super.afterFirstBuild(context);
    state = WM3u8State.loading;
  }

  @override
  Widget build(BuildContext context) {
    super.build(context);
    return _state == null
        ? Container()
        : Stack(
            children: <Widget>[
              if (_state != WM3u8State.error && _state != WM3u8State.completed)
                _WM3u8(
                  streamUrl: widget.streamUrl,
                  onStarted: () {
                    state = WM3u8State.started;
                  },
                  onPaused: () {
                    state = WM3u8State.paused;
                  },
                  onError: () {
                    state = WM3u8State.error;
                  },
                  onCompleted: () {
                    state = WM3u8State.completed;
                  },
                  controller: controller,
                ),
              if (_state == WM3u8State.error || _state == WM3u8State.completed)
                Container(
                    margin: EdgeInsets.symmetric(horizontal: 60.W),
                    alignment: Alignment.center,
                    child: Text(
                      'THE STREAM IS NOT LIVE YET OR IT IS IDLE.\nWHEN IT\'S LIVE YOU\'LL SEE IT HERE',
                      textAlign: TextAlign.center,
                      style: normalTextStyle(20.SP,
                          color: Colors.white,
                          fontFamily: appTheme.assets.fontLeaguegothic),
                    )),
              if (_state == WM3u8State.loading) _loadingView()
            ],
          );
  }

  // Build loading
  Widget _loadingView() {
    return Container(
      color: Colors.black12,
      alignment: Alignment.center,
      child: SizedBox(
        height: 50.H,
        width: 50.H,
        child: const CircularProgressIndicator(
          backgroundColor: Colors.white,
        ),
      ),
    );
  }
}

class _WM3u8 extends StatefulWidget {
  const _WM3u8({
    @required this.streamUrl,
    this.onStarted,
    this.onPaused,
    this.onError,
    this.onCompleted,
    this.controller,
    Key key,
  }) : super(key: key);

  final String streamUrl;
  final Function onStarted;
  final Function onPaused;
  final Function onError;
  final Function onCompleted;
  final WM3u8Controller controller;

  @override
  _WM3u8State createState() => _WM3u8State();
}

class _WM3u8State extends BaseStateful<_WM3u8> {
  WM3u8Controller get controller => widget.controller;
  final FijkPlayer player = FijkPlayer();

  @override
  void initState() {
    super.initState();
    controller.player = player;
  }

  @override
  void afterFirstBuild(BuildContext context) {
    super.afterFirstBuild(context);
    player.addListener(() {
      final FijkState state = player.value.state;
      print('updatestate: $state');
      if (state == FijkState.error && widget.onError != null) {
        widget.onError();
      } else if (state == FijkState.started && widget.onStarted != null) {
        widget.onStarted();
      } else if (state == FijkState.paused && widget.onPaused != null) {
        widget.onPaused();
      } else if (state == FijkState.completed && widget.onCompleted != null) {
        widget.onCompleted();
      }
    });
    // Start player
    // https://fijkplayer.befovy.com/docs/en/fijkstate.html#gsc.tab=0
    player.setDataSource(widget.streamUrl, autoPlay: true);
  }

  @override
  void dispose() {
    player.release().then((_) {
      player.dispose();
    });
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    super.build(context);
    return FijkView(
      player: player,
      color: Colors.black12,
      fit: (player.value?.size?.aspectRatio ?? 2) > 1
          ? FijkFit.contain
          : FijkFit.cover,
    );
  }
}
import 'dart:math';

import 'package:fijkplayer/fijkplayer.dart';
import 'package:flutter/material.dart';
import 'package:nft/services/safety/base_stateful.dart';
import 'package:nft/utils/app_extension.dart';

class WM3u8Panel extends StatefulWidget {
  const WM3u8Panel({
    @required this.player,
    @required this.isPlaying,
  });

  final FijkPlayer player;
  final bool isPlaying;

  @override
  _WM3u8PanelState createState() => _WM3u8PanelState();
}

class _WM3u8PanelState extends BaseStateful<WM3u8Panel> {
  FijkPlayer get player => widget.player;

  bool get isPlaying => widget.isPlaying ?? false;
  double progressValueStart = 0;
  double progressValue = 0;
  bool isChangeProgress = false;

  @override
  Widget build(BuildContext context) {
    super.build(context);
    return player == null
        ? Container(color: Colors.transparent)
        : Container(
            height: 60.H,
            child: Row(
              crossAxisAlignment: CrossAxisAlignment.center,
              children: <Widget>[
                IconButton(
                  icon: Icon(
                    isPlaying
                        ? Icons.pause_circle_filled
                        : Icons.play_circle_filled,
                  ),
                  onPressed: () {
                    isPlaying ? player.pause() : player.start();
                  },
                ),
                Expanded(
                  child: StreamBuilder<Duration>(
                    stream: player.onCurrentPosUpdate,
                    builder: (_, AsyncSnapshot<Duration> durationSnapshot) {
                      Duration duration = Duration.zero;
                      if (durationSnapshot.hasError ||
                          durationSnapshot.hasData) {
                        duration = durationSnapshot.data;
                      }
                      final double m3u8PlayerProgress =
                          duration.inMilliseconds.toDouble();
                      final bool isUpward = progressValueStart <= progressValue;
                      return FijkSlider(
                        colors: const FijkSliderColors(
                            bufferedColor: Colors.white54),
                        cacheValue: player.bufferPos.inMilliseconds.toDouble(),
                        max: max(
                            player.value.duration.inMilliseconds.toDouble(), 1),
                        value: (!isChangeProgress &&
                                (isUpward
                                    ? progressValue <= m3u8PlayerProgress
                                    : progressValue > m3u8PlayerProgress))
                            ? m3u8PlayerProgress
                            : progressValue,
                        onChanged: (double value) {
                          progressValue = value;
                          setState(() {});
                        },
                        onChangeStart: (double value) {
                          progressValue =
                              player.currentPos.inMilliseconds.toDouble();
                          progressValueStart = progressValue;
                          isChangeProgress = true;
                          setState(() {});
                        },
                        onChangeEnd: (double value) {
                          player.seekTo(value.toInt());
                          // Update
                          progressValue = value;
                          isChangeProgress = false;
                          setState(() {});
                        },
                      );
                    },
                  ),
                ),
                SizedBox(width: 15.W),
              ],
            ),
          );
  }
}

The source highlighted calling play after initialization when only an initial frame appeared. Readiness, actual playback, live/idle state, and controller disposal still need separate checks. The widgets depend on application-specific base-state and styling helpers.

Source: Gist revision. No video was published, played, or requested by executing these examples.

← Back to the garden