blob: a307bde42d1a80caf70eeab57abf6f6b9928eab4 [file] [log] [blame]
// Copyright 2017 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:io' as io;
import 'package:fidl/fidl.dart';
import 'package:fidl_fuchsia_media/fidl.dart' as media;
import 'package:fidl_fuchsia_media_playback/fidl.dart' as playback;
import 'package:fidl_fuchsia_modular/fidl.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:fuchsia/fuchsia.dart';
import 'package:lib.app.dart/app.dart';
import 'package:lib.mediaplayer.flutter/media_player.dart';
import 'package:lib.mediaplayer.flutter/media_player_controller.dart';
import 'asset.dart';
import 'config.dart';
final StartupContext _context = new StartupContext.fromStartupInfo();
final MediaPlayerController _controller =
new MediaPlayerController(_context.environmentServices);
ModuleImpl _module = new ModuleImpl();
void _log(String msg) {
print('[mediaplayer_flutter Module] $msg');
}
/// An implementation of the [Lifecycle] interface, which controls the lifetime
/// of the module.
class ModuleImpl implements Lifecycle {
final LifecycleBinding _lifecycleBinding = new LifecycleBinding();
/// Binds an [InterfaceRequest] for a [Lifecycle] interface to this object.
void bindLifecycle(InterfaceRequest<Lifecycle> request) {
_lifecycleBinding.bind(this, request);
}
/// Implementation of Lifecycle.Terminate method.
@override
void terminate() {
_log('ModuleImpl::Terminate call');
_lifecycleBinding.close();
exit(0);
}
}
const List<String> _configFileNames = const <String>[
'/data/mediaplayer_flutter.config',
'/pkg/data/mediaplayer_flutter.config',
];
List<Asset> _assets = <Asset>[];
Asset _assetToPlay;
Asset _leafAssetToPlay;
int _playlistIndex;
Future<Null> _readConfig() async {
for (String fileName in _configFileNames) {
try {
_assets = await readConfig(fileName);
return;
// ignore: avoid_catching_errors
} on ArgumentError {
// File doesn't exist. Continue.
} on FormatException catch (e) {
print('Failed to parse config $fileName: $e');
io.exit(0);
return;
}
}
print('No config file found');
io.exit(0);
}
/// Plays the specified asset.
void _play(Asset asset) {
assert(asset != null);
_assetToPlay = asset;
_playlistIndex = 0;
if (_assetToPlay.children != null) {
assert(_assetToPlay.children.isNotEmpty);
_playLeafAsset(_assetToPlay.children[0]);
} else {
_playLeafAsset(_assetToPlay);
}
}
/// If [_leafAssetToPlay] is looped, this method seeks to the beginning of the
/// asset and returns true. If [_assetToPlay] is a playlist and hasn't been
/// played through (or is looped), this method plays the next asset in
/// [_assetToPlay] and returns true. Returns false otherwise.
bool _playNext() {
if (_leafAssetToPlay.loop) {
// Looping leaf asset. Seek to the beginning.
_controller.seek(Duration.zero);
return true;
}
if (_assetToPlay == null || _assetToPlay.children == null) {
return false;
}
if (_assetToPlay.children.length <= ++_playlistIndex) {
if (!_assetToPlay.loop) {
return false;
}
// Looping playlist. Start over.
_playlistIndex = 0;
}
_playLeafAsset(_assetToPlay.children[_playlistIndex]);
return true;
}
void _playLeafAsset(Asset asset) {
assert(asset.type != AssetType.playlist);
_leafAssetToPlay = asset;
if (_controller.problem?.type == playback.problemConnectionFailed) {
_controller.close();
}
_controller
..open(_leafAssetToPlay.uri)
..play();
}
/// Screen for asset playback.
class _PlaybackScreen extends StatefulWidget {
const _PlaybackScreen({Key key}) : super(key: key);
@override
_PlaybackScreenState createState() => new _PlaybackScreenState();
}
class _PlaybackScreenState extends State<_PlaybackScreen> {
@override
void initState() {
_controller.addListener(_handleControllerChanged);
super.initState();
}
@override
void dispose() {
_controller.removeListener(_handleControllerChanged);
super.dispose();
}
/// Handles change notifications from the controller.
void _handleControllerChanged() {
setState(() {
if (_controller.ended) {
_playNext();
}
});
}
/// Adds a label to list [to] if [label] isn't null.
void _addLabel(String label, Color color, double fontSize, List<Widget> to) {
if (label == null) {
return;
}
to.add(new Container(
margin: const EdgeInsets.only(left: 10.0),
child: new Text(
label,
style: new TextStyle(color: color, fontSize: fontSize),
),
));
}
/// Adds a problem description to list [to] if there is a problem.
void _addProblem(List<Widget> to) {
playback.Problem problem = _controller.problem;
if (problem != null) {
String text;
if (problem.details != null && problem.details.isNotEmpty) {
text = problem.details;
} else {
switch (problem.type) {
case playback.problemInternal:
text = 'Internal error';
break;
case playback.problemAssetNotFound:
text = 'The requested content was not found';
break;
case playback.problemContainerNotSupported:
text = 'The requested content uses an unsupported container format';
break;
case playback.problemAudioEncodingNotSupported:
text = 'The requested content uses an unsupported audio encoding';
break;
case playback.problemVideoEncodingNotSupported:
text = 'The requested content uses an unsupported video encoding';
break;
case playback.problemConnectionFailed:
text = 'Connection to player failed';
break;
default:
text = 'Unrecognized problem type ${problem.type}';
break;
}
}
_addLabel(text, Colors.white, 20.0, to);
if (_leafAssetToPlay != null && _leafAssetToPlay.uri != null) {
_addLabel(_leafAssetToPlay.uri.toString(), Colors.grey[800], 15.0, to);
}
}
}
@override
Widget build(BuildContext context) {
List<Widget> columnChildren = <Widget>[
new MediaPlayer(_controller),
];
Map<String, String> metadata = _controller.metadata;
if (metadata != null) {
_addLabel(
metadata[media.metadataLabelTitle] ??
_leafAssetToPlay.title ??
'(untitled)',
Colors.white,
20.0,
columnChildren);
_addLabel(
metadata[media.metadataLabelArtist] ?? _leafAssetToPlay.artist,
Colors.grey[600],
15.0,
columnChildren);
_addLabel(
metadata[media.metadataLabelAlbum] ?? _leafAssetToPlay.album,
Colors.grey[800],
15.0,
columnChildren);
}
_addProblem(columnChildren);
return new Material(
color: Colors.black,
child: new Stack(
children: <Widget>[
new Positioned(
left: 0.0,
right: 0.0,
top: 0.0,
child: new Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: columnChildren,
),
),
new Positioned(
right: 0.0,
top: 0.0,
child: new Offstage(
offstage: !_controller.shouldShowControlOverlay,
child: new PhysicalModel(
elevation: 2.0,
color: Colors.transparent,
borderRadius: new BorderRadius.circular(60.0),
child: new IconButton(
icon: new Icon(
_assets.length == 1 ? Icons.close : Icons.arrow_back),
iconSize: 60.0,
onPressed: () {
if (_assets.length == 1) {
io.exit(0);
return;
}
_controller.pause();
Navigator.of(context).pop();
},
color: Colors.white,
),
),
),
),
],
),
);
}
}
/// Screen for asset selection
class _ChooserScreen extends StatefulWidget {
const _ChooserScreen({Key key}) : super(key: key);
@override
_ChooserScreenState createState() => new _ChooserScreenState();
}
class _ChooserScreenState extends State<_ChooserScreen> {
Widget _buildChooseButton(Asset asset) {
IconData iconData;
switch (asset.type) {
case AssetType.movie:
iconData = Icons.movie;
break;
case AssetType.song:
iconData = Icons.music_note;
break;
case AssetType.playlist:
iconData = Icons.playlist_play;
break;
}
return new RaisedButton(
onPressed: () {
_play(asset);
Navigator.of(context).pushNamed('/play');
},
color: Colors.black,
child: new Row(
children: <Widget>[
new Icon(
iconData,
size: 60.0,
color: Colors.grey[200],
),
new Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
new Text(
asset.title ?? '(no title)',
style: new TextStyle(color: Colors.grey[200], fontSize: 18.0),
),
new Text(
asset.artist ?? '',
style: new TextStyle(color: Colors.grey[600], fontSize: 13.0),
),
new Text(
asset.album ?? '',
style: new TextStyle(color: Colors.grey[800], fontSize: 13.0),
),
],
),
],
),
);
}
@override
Widget build(BuildContext context) {
return new Material(
color: Colors.black,
child: new Stack(
children: <Widget>[
new ListView(
itemExtent: 75.0,
children: _assets.map(_buildChooseButton).toList(),
),
new Positioned(
right: 0.0,
top: 0.0,
child: new PhysicalModel(
elevation: 2.0,
color: Colors.transparent,
child: new IconButton(
icon: const Icon(Icons.close),
iconSize: 60.0,
onPressed: () {
io.exit(0);
},
color: Colors.white,
),
),
),
],
),
);
}
}
Future<Null> main() async {
_log('Module started');
/// Add [ModuleImpl] to this application's outgoing ServiceProvider.
_context.outgoingServices.addServiceForName(
(InterfaceRequest<Lifecycle> request) {
_module.bindLifecycle(request);
},
Lifecycle.$serviceName,
);
await _readConfig();
if (_assets.isEmpty) {
print('no assets configured');
return;
}
if (_assets.length == 1) {
_play(_assets[0]);
}
runApp(new MaterialApp(
title: 'Media Player',
home:
_assets.length == 1 ? const _PlaybackScreen() : const _ChooserScreen(),
routes: <String, WidgetBuilder>{
'/play': (BuildContext context) => const _PlaybackScreen()
},
theme: new ThemeData(primarySwatch: Colors.blue),
debugShowCheckedModeBanner: false,
));
}