Add audioplayers for elinux (#96)

Signed-off-by: Makoto Sato <makoto.sato@atmark-techno.com>
This commit is contained in:
Makoto Sato
2024-05-02 10:59:55 +09:00
committed by GitHub
parent d711760ef0
commit 10a89e1421
53 changed files with 4279 additions and 0 deletions

View File

@ -17,6 +17,7 @@ The plugins for elinux are basically designed to be API compatible with the offi
| ------------------ | ---------------- |
| [video_player_elinux](packages/video_player) | [video_player](https://github.com/flutter/packages/tree/main/packages/video_player/video_player) |
| [camera_elinux](packages/camera) | [camera](https://github.com/flutter/packages/tree/main/packages/camera/camera) |
| [audioplayers_elinux](packages/audioplayers) | [audioplayers](https://github.com/bluefireteam/audioplayers/tree/main/packages/audioplayers)
| [path_provider_elinux](packages/path_provider) | [path_provider](https://github.com/flutter/packages/tree/main/packages/path_provider) |
| [shared_preferences_elinux](packages/shared_preferences) | [shared_preferences](https://github.com/flutter/packages/tree/main/packages/shared_preferences) |
| [joystick](packages/joystick) | - |

7
packages/audioplayers/.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
.DS_Store
.dart_tool/
.packages
.pub/
build/

View File

@ -0,0 +1,2 @@
## 0.1.0
* First draft version.

View File

@ -0,0 +1,27 @@
Copyright (c) 2024 Sony Group Corporation. All rights reserved.
Copyright (c) 2013 The Flutter Authors. All rights reserved.
Copyright (c) 2017 Luan Nico
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
* Neither the names of the copyright holders nor the names of the
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -0,0 +1,35 @@
# audioplayers_elinux
The implementation of the Audio Player plugin for flutter elinux. APIs are designed to be API compatible with the the official [`audioplayers`](https://github.com/bluefireteam/audioplayers/tree/main/packages/audioplayers).
## Required libraries
This plugin uses [GStreamer](https://gstreamer.freedesktop.org/) internally.
```Shell
$ sudo apt install libglib2.0-dev
$ sudo apt install libgstreamer1.0-dev
# Install as needed.
$ sudo apt install libgstreamer-plugins-base1.0-dev \
gstreamer1.0-plugins-base gstreamer1.0-plugins-good \
gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav
```
## Usage
### pubspec.yaml
```yaml
dependencies:
audioplayers: ^6.0.0
audioplayers_elinux:
git:
url: https://github.com/sony/flutter-elinux-plugins.git
path: packages/audioplayers
ref: main
```
### Source code
Import `audioplayers` in your Dart code:
```dart
import 'package:audioplayers/audioplayers.dart';
```

View File

@ -0,0 +1,41 @@
cmake_minimum_required(VERSION 3.15)
set(PROJECT_NAME "audioplayers_elinux")
project(${PROJECT_NAME} LANGUAGES CXX)
# This value is used when generating builds using this plugin, so it must
# not be changed
set(PLUGIN_NAME "audioplayers_elinux_plugin")
find_package(PkgConfig)
pkg_check_modules(GLIB REQUIRED glib-2.0)
pkg_check_modules(GSTREAMER REQUIRED gstreamer-1.0)
add_library(${PLUGIN_NAME} SHARED
"audioplayers_elinux_plugin.cc"
"gst_audio_player.cc"
)
apply_standard_settings(${PLUGIN_NAME})
set_target_properties(${PLUGIN_NAME} PROPERTIES
CXX_VISIBILITY_PRESET hidden)
target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL)
target_include_directories(${PLUGIN_NAME} INTERFACE
"${CMAKE_CURRENT_SOURCE_DIR}/include")
target_link_libraries(${PLUGIN_NAME} PRIVATE flutter flutter_wrapper_plugin)
target_include_directories(${PLUGIN_NAME}
PRIVATE
${GLIB_INCLUDE_DIRS}
${GSTREAMER_INCLUDE_DIRS}
)
target_link_libraries(${PLUGIN_NAME}
PRIVATE
${GLIB_LIBRARIES}
${GSTREAMER_LIBRARIES}
)
# List of absolute paths to libraries that should be bundled with the plugin
set(audioplayers_elinux_bundled_libraries
""
PARENT_SCOPE
)

View File

@ -0,0 +1,55 @@
// Copyright 2024 Sony Group Corporation. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef PACKAGES_AUDIOPLAYERS_AUDIOPLAYERS_ELINUX_AUDIO_PLAYER_STREAM_HANDLER_H_
#define PACKAGES_AUDIOPLAYERS_AUDIOPLAYERS_ELINUX_AUDIO_PLAYER_STREAM_HANDLER_H_
#include <string>
class AudioPlayerStreamHandler {
public:
AudioPlayerStreamHandler() = default;
virtual ~AudioPlayerStreamHandler() = default;
// Prevent copying.
AudioPlayerStreamHandler(AudioPlayerStreamHandler const&) = delete;
AudioPlayerStreamHandler& operator=(AudioPlayerStreamHandler const&) = delete;
// Notifies the completion of preparation the audio player.
void OnNotifyPrepared(const std::string &player_id,
const bool is_prepared) {
OnNotifyPreparedInternal(player_id, is_prepared);
}
// Notifies the duration of an audio.
void OnNotifyDuration(const std::string &player_id,
const int32_t duration) {
OnNotifyDurationInternal(player_id, duration);
}
// Notifies the completion of seeks an audio.
void OnNotifySeekCompleted(const std::string &player_id) {
OnNotifySeekCompletedInternal(player_id);
}
// Notifies the completion of playing an audio.
void OnNotifyPlayCompleted(const std::string &player_id) {
OnNotifyPlayCompletedInternal(player_id);
}
// Notifies the log of the audio player.
void OnNotifyLog(const std::string &player_id,
const std::string &message) {
OnNotifyLogInternal(player_id, message);
}
protected:
virtual void OnNotifyPreparedInternal(const std::string&, const bool) = 0;
virtual void OnNotifyDurationInternal(const std::string&, const int32_t) = 0;
virtual void OnNotifySeekCompletedInternal(const std::string&) = 0;
virtual void OnNotifyPlayCompletedInternal(const std::string &) = 0;
virtual void OnNotifyLogInternal(const std::string&, const std::string&) = 0;
};
#endif // PACKAGES_AUDIOPLAYERS_AUDIOPLAYERS_ELINUX_AUDIO_PLAYER_STREAM_HANDLER_H_

View File

@ -0,0 +1,85 @@
// Copyright 2024 Sony Group Corporation. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef PACKAGES_AUDIOPLAYERS_AUDIOPLAYERS_ELINUX_AUDIO_PLAYER_STREAM_HANDLER_IMPL_H_
#define PACKAGES_AUDIOPLAYERS_AUDIOPLAYERS_ELINUX_AUDIO_PLAYER_STREAM_HANDLER_IMPL_H_
#include <functional>
#include "audio_player_stream_handler.h"
class AudioPlayerStreamHandlerImpl : public AudioPlayerStreamHandler {
public:
using OnNotifyPrepared = std::function<void(const std::string&, const bool)>;
using OnNotifyDuration =
std::function<void(const std::string&, const int32_t)>;
using OnNotifySeekCompleted = std::function<void(const std::string&)>;
using OnNotifyPlayCompleted = std::function<void(const std::string&)>;
using OnNotifyLog =
std::function<void(const std::string&, const std::string&)>;
AudioPlayerStreamHandlerImpl(OnNotifyPrepared on_notify_prepared,
OnNotifyDuration on_notify_duration,
OnNotifySeekCompleted on_notify_seek_completed,
OnNotifyPlayCompleted on_notify_play_completed,
OnNotifyLog on_notify_log)
: on_notify_prepared_(on_notify_prepared),
on_notify_duration_(on_notify_duration),
on_notify_seek_completed_(on_notify_seek_completed),
on_notify_play_completed_(on_notify_play_completed),
on_notify_log_(on_notify_log) {}
virtual ~AudioPlayerStreamHandlerImpl() = default;
// Prevent copying.
AudioPlayerStreamHandlerImpl(AudioPlayerStreamHandlerImpl const&) = delete;
AudioPlayerStreamHandlerImpl& operator=(AudioPlayerStreamHandlerImpl const&) =
delete;
protected:
// |AudioPlayerStreamHandler|
void OnNotifyPreparedInternal(const std::string &player_id,
const bool is_prepared) {
if (on_notify_prepared_) {
on_notify_prepared_(player_id, is_prepared);
}
}
// |AudioPlayerStreamHandler|
void OnNotifyDurationInternal(const std::string &player_id,
const int32_t duration) {
if (on_notify_duration_) {
on_notify_duration_(player_id, duration);
}
}
// |AudioPlayerStreamHandler|
void OnNotifySeekCompletedInternal(const std::string &player_id) {
if (on_notify_seek_completed_) {
on_notify_seek_completed_(player_id);
}
}
// |AudioPlayerStreamHandler|
void OnNotifyPlayCompletedInternal(const std::string &player_id) {
if (on_notify_play_completed_) {
on_notify_play_completed_(player_id);
}
}
// |AudioPlayerStreamHandler|
void OnNotifyLogInternal(const std::string &player_id,
const std::string &message) {
if (on_notify_log_) {
on_notify_log_(player_id, message);
}
}
OnNotifyPrepared on_notify_prepared_;
OnNotifyDuration on_notify_duration_;
OnNotifySeekCompleted on_notify_seek_completed_;
OnNotifyPlayCompleted on_notify_play_completed_;
OnNotifyLog on_notify_log_;
};
#endif // PACKAGES_AUDIOPLAYERS_AUDIOPLAYERS_ELINUX_AUDIO_PLAYER_STREAM_HANDLER_IMPL_H_

View File

@ -0,0 +1,307 @@
// Copyright 2024 Sony Group Corporation. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "include/audioplayers_elinux/audioplayers_elinux_plugin.h"
#include <flutter/basic_message_channel.h>
#include <flutter/encodable_value.h>
#include <flutter/event_channel.h>
#include <flutter/event_sink.h>
#include <flutter/event_stream_handler_functions.h>
#include <flutter/method_channel.h>
#include <flutter/plugin_registrar.h>
#include <flutter/standard_message_codec.h>
#include <flutter/standard_method_codec.h>
#include <map>
#include <memory>
#include <string>
#include <variant>
#include "gst_audio_player.h"
#include "audio_player_stream_handler_impl.h"
namespace {
constexpr char kInvalidArgument[] = "Invalid argument";
constexpr char kAudioDurationEvent[] = "audio.onDuration";
constexpr char kAudioPreparedEvent[] = "audio.onPrepared";
constexpr char kAudioSeekCompleteEvent[] = "audio.onSeekComplete";
constexpr char kAudioCompleteEvent[] = "audio.onComplete";
constexpr char kAudioLogEvent[] = "audio.onLog";
template <typename T>
bool GetValueFromEncodableMap(const flutter::EncodableMap* map, const char* key,
T &out) {
auto iter = map->find(flutter::EncodableValue(key));
if (iter != map->end() && !iter->second.IsNull()) {
if (auto* value = std::get_if<T>(&iter->second)) {
out = *value;
return true;
}
}
return false;
}
class AudioplayersElinuxPlugin : public flutter::Plugin {
public:
static void RegisterWithRegistrar(flutter::PluginRegistrar* registrar) {
auto plugin = std::make_unique<AudioplayersElinuxPlugin>(registrar);
{
auto channel =
std::make_unique<flutter::MethodChannel<flutter::EncodableValue>>(
registrar->messenger(), "xyz.luan/audioplayers",
&flutter::StandardMethodCodec::GetInstance());
channel->SetMethodCallHandler(
[plugin_pointer = plugin.get()](const auto &call, auto result) {
plugin_pointer->HandleMethodCall(call, std::move(result));
});
}
{
auto channel =
std::make_unique<flutter::MethodChannel<flutter::EncodableValue>>(
registrar->messenger(), "xyz.luan/audioplayers.global",
&flutter::StandardMethodCodec::GetInstance());
channel->SetMethodCallHandler(
[plugin_pointer = plugin.get()](const auto &call, auto result) {
plugin_pointer->HandleGlobalMethodCall(call, std::move(result));
});
}
registrar->AddPlugin(std::move(plugin));
}
AudioplayersElinuxPlugin(flutter::PluginRegistrar* registrar)
: registrar_(registrar) {
GstAudioPlayer::GstLibraryLoad();
}
virtual ~AudioplayersElinuxPlugin() {}
void SetRegistrar(flutter::PluginRegistrar* registrar) {
registrar_ = registrar;
}
private:
void HandleMethodCall(
const flutter::MethodCall<flutter::EncodableValue>& method_call,
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {
const auto* arguments =
std::get_if<flutter::EncodableMap>(method_call.arguments());
if (!arguments) {
result->Error(kInvalidArgument, "No arguments provided.");
return;
}
std::string player_id;
if (!GetValueFromEncodableMap(arguments, "playerId", player_id)) {
result->Error(kInvalidArgument, "No playerId provided.");
return;
}
const std::string &method_name = method_call.method_name();
if (method_name == "create") {
CreateAudioPlayer(player_id);
result->Success();
return;
}
GstAudioPlayer* player = GetAudioPlayer(player_id);
if (!player) {
result->Error(kInvalidArgument,
"No AudioPlayer" + player_id + " is exist.");
return;
}
if (method_name == "resume") {
player->Resume();
result->Success();
} else if (method_name == "pause") {
player->Pause();
result->Success();
} else if (method_name == "stop") {
player->Stop();
result->Success();
} else if (method_name == "release") {
player->Release();
result->Success();
} else if (method_name == "seek") {
int32_t position = 0;
GetValueFromEncodableMap(arguments, "position", position);
player->Seek(position);
result->Success();
} else if (method_name == "setVolume") {
double volume = 0;
GetValueFromEncodableMap(arguments, "volume", volume);
player->SetVolume(volume);
result->Success();
} else if (method_name == "setSourceUrl") {
bool is_local = false;
GetValueFromEncodableMap(arguments, "isLocal", is_local);
std::string url = "";
GetValueFromEncodableMap(arguments, "url", url);
if (is_local) {
url = std::string("file://") + url;
}
player->SetSourceUrl(url);
result->Success();
} else if (method_name == "setPlaybackRate") {
double rate = 0;
GetValueFromEncodableMap(arguments, "playbackRate", rate);
player->SetPlaybackRate(rate);
result->Success();
} else if (method_name == "setReleaseMode") {
std::string release_mode = "";
GetValueFromEncodableMap(arguments, "releaseMode", release_mode);
bool looping = release_mode.find("loop") != std::string::npos;
player->SetLooping(looping);
result->Success();
} else if (method_name == "getDuration") {
int64_t duration = player->GetDuration();
if (duration >= 0) {
result->Success(flutter::EncodableValue(duration));
} else {
result->Success();
}
} else if (method_name == "getCurrentPosition") {
int64_t position = player->GetCurrentPosition();
if (position >= 0) {
result->Success(flutter::EncodableValue(position));
} else {
result->Success();
}
} else if (method_name == "setBalance") {
double balance = 0;
GetValueFromEncodableMap(arguments, "balance", balance);
player->SetBalance(balance);
result->Success();
}
else if (method_name == "setPlayerMode") {
result->NotImplemented();
} else if (method_name == "setAudioContext") {
result->NotImplemented();
} else if (method_name == "emitLog") {
result->NotImplemented();
} else if (method_name == "emitError") {
result->NotImplemented();
} else if (method_name == "dispose") {
player->Dispose();
audio_players_.erase(player_id);
event_sinks_.erase(player_id);
result->Success();
} else {
result->NotImplemented();
}
}
void HandleGlobalMethodCall(
const flutter::MethodCall<flutter::EncodableValue>& method_call,
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {
const std::string &method_name = method_call.method_name();
if (method_name == "setAudioContext") {
result->NotImplemented();
} else if (method_name == "emitLog") {
result->NotImplemented();
} else if (method_name == "emitError") {
result->NotImplemented();
} else {
result->NotImplemented();
}
}
GstAudioPlayer* GetAudioPlayer(const std::string &player_id) {
auto iter = audio_players_.find(player_id);
if (iter != audio_players_.end()) {
return iter->second.get();
}
return nullptr;
}
void CreateAudioPlayer(const std::string &player_id) {
auto event_channel =
std::make_unique<flutter::EventChannel<flutter::EncodableValue>>(
registrar_->messenger(),
"xyz.luan/audioplayers/events/" + player_id,
&flutter::StandardMethodCodec::GetInstance());
auto event_channel_handler = std::make_unique<
flutter::StreamHandlerFunctions<flutter::EncodableValue>>(
// StreamHandlerFunctions
[this, id = player_id](
const flutter::EncodableValue* arguments,
std::unique_ptr<flutter::EventSink<flutter::EncodableValue>>&&
events)
-> std::unique_ptr<
flutter::StreamHandlerError<flutter::EncodableValue>> {
this->event_sinks_[id] = std::move(events);
return nullptr;
},
// StreamHandlerCancel
[](const flutter::EncodableValue* arguments)
-> std::unique_ptr<
flutter::StreamHandlerError<flutter::EncodableValue>> {
return nullptr;
});
event_channel->SetStreamHandler(std::move(event_channel_handler));
auto player_handler = std::make_unique<AudioPlayerStreamHandlerImpl>(
// OnNotifyPrepared
[this](const std::string &player_id, bool is_prepared) {
flutter::EncodableMap map = {
{flutter::EncodableValue("event"),
flutter::EncodableValue(kAudioPreparedEvent)},
{flutter::EncodableValue("value"),
flutter::EncodableValue(is_prepared)}};
event_sinks_[player_id]->Success(flutter::EncodableValue(map));
},
// OnNotifyDuration
[this](const std::string &player_id, int32_t duration) {
flutter::EncodableMap map = {
{flutter::EncodableValue("event"),
flutter::EncodableValue(kAudioDurationEvent)},
{flutter::EncodableValue("value"),
flutter::EncodableValue(duration)}};
event_sinks_[player_id]->Success(flutter::EncodableValue(map));
},
// OnNotifySeekCompleted
[this](const std::string &player_id) {
flutter::EncodableMap map = {
{flutter::EncodableValue("event"),
flutter::EncodableValue(kAudioSeekCompleteEvent)}};
event_sinks_[player_id]->Success(flutter::EncodableValue(map));
},
// OnNotifyPlayCompleted
[this](const std::string &player_id) {
flutter::EncodableMap map = {
{flutter::EncodableValue("event"),
flutter::EncodableValue(kAudioCompleteEvent)}};
event_sinks_[player_id]->Success(flutter::EncodableValue(map));
},
// OnNotifyLog
[this](const std::string &player_id, const std::string &message) {
flutter::EncodableMap map = {
{flutter::EncodableValue("event"),
flutter::EncodableValue(kAudioLogEvent)},
{flutter::EncodableValue("value"),
flutter::EncodableValue(message)}};
event_sinks_[player_id]->Success(flutter::EncodableValue(map));
});
auto player =
std::make_unique<GstAudioPlayer>(player_id, std::move(player_handler));
audio_players_[player_id] = std::move(player);
}
std::map<std::string, std::unique_ptr<GstAudioPlayer>> audio_players_;
std::map<std::string,
std::unique_ptr<flutter::EventSink<flutter::EncodableValue>>>
event_sinks_;
flutter::PluginRegistrar* registrar_;
};
} // namespace
void AudioplayersElinuxPluginRegisterWithRegistrar(
FlutterDesktopPluginRegistrarRef registrar) {
AudioplayersElinuxPlugin::RegisterWithRegistrar(
flutter::PluginRegistrarManager::GetInstance()
->GetRegistrar<flutter::PluginRegistrar>(registrar));
}

View File

@ -0,0 +1,370 @@
// Copyright 2024 Sony Group Corporation. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "gst_audio_player.h"
#include <iostream>
GstAudioPlayer::GstAudioPlayer(
const std::string &player_id,
std::unique_ptr<AudioPlayerStreamHandler> handler)
: player_id_(player_id),
stream_handler_(std::move(handler)) {
gst_.playbin = nullptr;
gst_.bus = nullptr;
gst_.source = nullptr;
gst_.panorama = nullptr;
gst_.audiobin = nullptr;
gst_.audiosink = nullptr;
gst_.panoramasinkpad = nullptr;
if (!CreatePipeline()) {
std::cerr << "Failed to create a pipeline" << std::endl;
Dispose();
return;
}
}
GstAudioPlayer::~GstAudioPlayer() {
Stop();
Dispose();
}
// static
void GstAudioPlayer::GstLibraryLoad() { gst_init(NULL, NULL); }
// static
void GstAudioPlayer::GstLibraryUnload() { gst_deinit(); }
// Creates a audio playbin.
// $ playbin uri=<file>
bool GstAudioPlayer::CreatePipeline() {
gst_.playbin = gst_element_factory_make("playbin", "playbin");
if (!gst_.playbin) {
std::cerr << "Failed to create a playbin" << std::endl;
return false;
}
// Setup stereo balance controller
gst_.panorama = gst_element_factory_make("audiopanorama", "audiopanorama");
if (gst_.panorama) {
gst_.audiobin = gst_bin_new(NULL);
gst_.audiosink = gst_element_factory_make("autoaudiosink", "autoaudiosink");
gst_bin_add_many(GST_BIN(gst_.audiobin), gst_.panorama, gst_.audiosink, NULL);
gst_element_link(gst_.panorama, gst_.audiosink);
GstPad* sinkpad = gst_element_get_static_pad(gst_.panorama, "sink");
gst_.panoramasinkpad = gst_ghost_pad_new("sink", sinkpad);
gst_element_add_pad(gst_.audiobin, gst_.panoramasinkpad);
gst_object_unref(GST_OBJECT(sinkpad));
g_object_set(G_OBJECT(gst_.playbin), "audio-sink", gst_.audiobin, NULL);
g_object_set(G_OBJECT(gst_.panorama), "method", 1, NULL);
}
// Setup source options
g_signal_connect(gst_.playbin, "source-setup",
G_CALLBACK(GstAudioPlayer::SourceSetup), &gst_.source);
// Watch bus messages for one time events
gst_.bus = gst_pipeline_get_bus(GST_PIPELINE(gst_.playbin));
gst_bus_set_sync_handler(gst_.bus, HandleGstMessage, this, NULL);
return true;
}
// static
void GstAudioPlayer::SourceSetup(GstElement* playbin,
GstElement* source,
GstElement** p_src) {
// Allow sources from unencrypted / misconfigured connections
if (g_object_class_find_property(
G_OBJECT_GET_CLASS(source), "ssl-strict") != 0) {
g_object_set(G_OBJECT(source), "ssl-strict", FALSE, NULL);
}
}
std::string GstAudioPlayer::ParseUri(const std::string& uri) {
if (gst_uri_is_valid(uri.c_str())) {
return uri;
}
const auto* filename_uri = gst_filename_to_uri(uri.c_str(), NULL);
if (!filename_uri) {
std::cerr << "Faild to open " << uri.c_str() << std::endl;
return uri;
}
std::string result_uri(filename_uri);
delete filename_uri;
return result_uri;
}
void GstAudioPlayer::Resume() {
if (!is_playing_) {
is_playing_ = true;
}
if (!is_initialized_) {
return;
}
if (gst_element_set_state(gst_.playbin, GST_STATE_PLAYING) ==
GST_STATE_CHANGE_FAILURE) {
std::cerr << "Unable to set the pipeline to GST_STATE_PLAYING" << std::endl;
return;
}
int64_t duration = GetDuration();
stream_handler_->OnNotifyDuration(player_id_, duration);
}
void GstAudioPlayer::Play() {
Seek(0);
Resume();
}
void GstAudioPlayer::Pause() {
if (is_playing_) {
is_playing_ = false;
}
if (!is_initialized_) {
return;
}
if (gst_element_set_state(gst_.playbin, GST_STATE_PAUSED) ==
GST_STATE_CHANGE_FAILURE) {
std::cerr << "Failed to change the state to PAUSED" << std::endl;
return;
}
}
void GstAudioPlayer::Stop() {
Pause();
if (!is_initialized_) {
return;
}
Seek(0);
}
void GstAudioPlayer::Seek(int64_t position) {
if (!is_initialized_) {
return;
}
auto nanosecond = position * 1000 * 1000;
if (!gst_element_seek(
gst_.playbin, playback_rate_, GST_FORMAT_TIME,
(GstSeekFlags)(GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_KEY_UNIT),
GST_SEEK_TYPE_SET, nanosecond, GST_SEEK_TYPE_SET,
GST_CLOCK_TIME_NONE)) {
std::cerr << "Failed to seek " << position << std::endl;
return;
}
stream_handler_->OnNotifySeekCompleted(player_id_);
}
void GstAudioPlayer::SetSourceUrl(std::string url) {
if (url_ != url) {
url_ = url;
// flush unhandled messeges
gst_bus_set_flushing(gst_.bus, TRUE);
gst_element_set_state(gst_.playbin, GST_STATE_NULL);
is_playing_ = false;
if (!url_.empty()) {
g_object_set(GST_OBJECT(gst_.playbin), "uri", url_.c_str(), NULL);
if (gst_.playbin->current_state != GST_STATE_READY) {
GstStateChangeReturn ret =
gst_element_set_state(gst_.playbin, GST_STATE_READY);
if (ret == GST_STATE_CHANGE_FAILURE) {
std::cerr <<
"Unable to set the pipeline to GST_STATE_READY." << std::endl;
}
}
}
is_initialized_ = true;
}
stream_handler_->OnNotifyPrepared(player_id_, true);
}
void GstAudioPlayer::SetVolume(double volume) {
if (volume > 1) {
volume = 1;
} else if (volume < 0) {
volume = 0;
}
volume_ = volume;
g_object_set(gst_.playbin, "volume", volume, NULL);
}
void GstAudioPlayer::SetBalance(double balance) {
if (!gst_.panorama) {
std::cerr << "Audiopanorama was not initialized" << std::endl;
return;
}
if (balance > 1.0) {
balance = 1.0;
} else if (balance < -1.0) {
balance = -1.0;
}
g_object_set(G_OBJECT(gst_.panorama), "panorama", balance, NULL);
}
void GstAudioPlayer::SetPlaybackRate(double playback_rate) {
if (playback_rate <= 0) {
std::cerr << "Rate " << playback_rate << " is not supported" << std::endl;
return;
}
if (!is_initialized_) {
return;
}
int64_t position = GetCurrentPosition();
if (!gst_element_seek(gst_.playbin, playback_rate, GST_FORMAT_TIME,
GST_SEEK_FLAG_FLUSH, GST_SEEK_TYPE_SET,
position * GST_MSECOND, GST_SEEK_TYPE_SET,
GST_CLOCK_TIME_NONE)) {
std::cerr << "Failed to set playback rate to " << playback_rate
<< " (gst_element_seek failed)" << std::endl;
return;
}
playback_rate_ = playback_rate;
}
void GstAudioPlayer::SetLooping(bool is_looping) {
is_looping_ = is_looping;
}
int64_t GstAudioPlayer::GetDuration() {
gint64 duration;
if (!gst_element_query_duration(gst_.playbin, GST_FORMAT_TIME, &duration)) {
return -1;
}
return duration /= GST_MSECOND;
}
int64_t GstAudioPlayer::GetCurrentPosition() {
gint64 position = 0;
if (!gst_element_query_position(gst_.playbin, GST_FORMAT_TIME, &position)) {
return -1;
}
// TODO: We need to handle this code in the proper plase.
// The VideoPlayer plugin doesn't have a main loop, so EOS message
// received from GStreamer cannot be processed directly in a callback
// function. This is because the event channel message of playback complettion
// needs to be thrown in the main thread.
if (is_completed_) {
is_completed_ = false;
if (is_looping_) {
Play();
} else {
stream_handler_->OnNotifyPlayCompleted(player_id_);
Stop();
}
position = 0;
}
return position / GST_MSECOND;
}
void GstAudioPlayer::Release() {
is_playing_ = false;
is_initialized_ = false;
url_.clear();
GstState state;
gst_element_get_state(gst_.playbin, &state, NULL, GST_CLOCK_TIME_NONE);
if (state > GST_STATE_NULL) {
gst_bus_set_flushing(gst_.bus, TRUE);
gst_element_set_state(gst_.playbin, GST_STATE_NULL);
}
}
void GstAudioPlayer::Dispose() {
if (!gst_.playbin) {
std::cerr << "Already disposed" << std::endl;
return;
}
is_playing_ = false;
is_initialized_ = false;
url_.clear();
if (gst_.bus) {
gst_bus_set_flushing(gst_.bus, TRUE);
gst_object_unref(GST_OBJECT(gst_.bus));
gst_.bus = nullptr;
}
if (gst_.source) {
gst_object_unref(GST_OBJECT(gst_.source));
gst_.source = nullptr;
}
if (gst_.panorama) {
gst_element_set_state(gst_.audiobin, GST_STATE_NULL);
gst_element_remove_pad(gst_.audiobin, gst_.panoramasinkpad);
gst_bin_remove(GST_BIN(gst_.audiobin), gst_.audiosink);
gst_bin_remove(GST_BIN(gst_.audiobin), gst_.panorama);
gst_.panorama = nullptr;
}
gst_.playbin = nullptr;
}
// static
GstBusSyncReply GstAudioPlayer::HandleGstMessage(GstBus* bus,
GstMessage* message,
gpointer user_data) {
auto* self = reinterpret_cast<GstAudioPlayer*>(user_data);
switch (GST_MESSAGE_TYPE(message)) {
case GST_MESSAGE_STATE_CHANGED: {
if (GST_MESSAGE_SRC(message) == GST_OBJECT(self->gst_.playbin)) {
GstState old_state, new_state;
gst_message_parse_state_changed(message, &old_state, &new_state, NULL);
if (new_state == GST_STATE_READY) {
if (gst_element_set_state(self->gst_.playbin, GST_STATE_PAUSED) ==
GST_STATE_CHANGE_FAILURE) {
g_printerr("Unable to set the pipeline from GST_STATE_READY "
"to GST_STATE_PAUSED\n");
}
}
}
break;
}
case GST_MESSAGE_EOS:
self->is_completed_ = true;
break;
case GST_MESSAGE_WARNING: {
gchar* debug;
GError* error;
gst_message_parse_warning(message, &error, &debug);
g_printerr("WARNING from element %s: %s\n", GST_OBJECT_NAME(message->src),
error->message);
g_printerr("Warning details: %s\n", debug);
g_free(debug);
g_error_free(error);
break;
}
case GST_MESSAGE_ERROR: {
gchar* debug;
GError* error;
gst_message_parse_error(message, &error, &debug);
g_printerr("ERROR from element %s: %s\n", GST_OBJECT_NAME(message->src),
error->message);
g_printerr("Error details: %s\n", debug);
g_free(debug);
g_error_free(error);
break;
}
default:
break;
}
gst_message_unref(message);
return GST_BUS_DROP;
}

View File

@ -0,0 +1,73 @@
// Copyright 2024 Sony Group Corporation. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef PACKAGES_AUDIOPLAYERS_AUDIOPLAYERS_ELINUX_GST_AUDIO_PLAYER_H_
#define PACKAGES_AUDIOPLAYERS_AUDIOPLAYERS_ELINUX_GST_AUDIO_PLAYER_H_
#include <gst/gst.h>
#include <functional>
#include <memory>
#include <mutex>
#include <vector>
#include <string>
#include "audio_player_stream_handler.h"
class GstAudioPlayer {
public:
GstAudioPlayer(const std::string &player_id,
std::unique_ptr<AudioPlayerStreamHandler> handler);
~GstAudioPlayer();
static void GstLibraryLoad();
static void GstLibraryUnload();
void Resume();
void Play();
void Pause();
void Stop();
void Seek(int64_t position);
void SetSourceUrl(std::string url);
void SetVolume(double volume);
void SetBalance(double balance);
void SetPlaybackRate(double playback_rate);
void SetLooping(bool is_looping);
int64_t GetDuration();
int64_t GetCurrentPosition();
void Release();
void Dispose();
private:
struct GstAudioElements {
GstElement* playbin;
GstBus* bus;
GstElement* source;
GstElement* panorama;
GstElement* audiobin;
GstElement* audiosink;
GstPad* panoramasinkpad;
};
static GstBusSyncReply HandleGstMessage(GstBus* bus, GstMessage* message,
gpointer user_data);
static void SourceSetup(GstElement* playbin,
GstElement* source,
GstElement** p_src);
bool CreatePipeline();
std::string ParseUri(const std::string& uri);
GstAudioElements gst_;
const std::string player_id_;
std::string url_;
bool is_initialized_ = false;
bool is_playing_ = false;
bool is_looping_ = false;
double volume_ = 1.0;
double playback_rate_ = 1.0;
bool is_completed_ = false;
std::unique_ptr<AudioPlayerStreamHandler> stream_handler_;
};
#endif // PACKAGES_AUDIOPLAYERS_AUDIOPLAYERS_ELINUX_GST_AUDIO_PLAYER_H_

View File

@ -0,0 +1,23 @@
#ifndef FLUTTER_PLUGIN_AUDIOPLAYERS_ELINUX_PLUGIN_H_
#define FLUTTER_PLUGIN_AUDIOPLAYERS_ELINUX_PLUGIN_H_
#include <flutter_plugin_registrar.h>
#ifdef FLUTTER_PLUGIN_IMPL
#define FLUTTER_PLUGIN_EXPORT __attribute__((visibility("default")))
#else
#define FLUTTER_PLUGIN_EXPORT
#endif
#if defined(__cplusplus)
extern "C" {
#endif
FLUTTER_PLUGIN_EXPORT void AudioplayersElinuxPluginRegisterWithRegistrar(
FlutterDesktopPluginRegistrarRef registrar);
#if defined(__cplusplus)
} // extern "C"
#endif
#endif // FLUTTER_PLUGIN_AUDIOPLAYERS_ELINUX_PLUGIN_H_

View File

@ -0,0 +1,8 @@
# audioplayers_example
Demonstrates how to use the audioplayers plugin.
## Getting Started
For help getting started with Flutter for eLinux, view our online
[documentation](https://github.com/sony/flutter-elinux/wiki).

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1 @@
This represents an invalid audio file.

Binary file not shown.

View File

@ -0,0 +1,103 @@
cmake_minimum_required(VERSION 3.15)
# stop cmake from taking make from CMAKE_SYSROOT
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
project(runner LANGUAGES CXX)
set(BINARY_NAME "audioplayers_example")
cmake_policy(SET CMP0063 NEW)
set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
# Basically we use this include when we got the following error:
# fatal error: 'bits/c++config.h' file not found
include_directories(SYSTEM ${FLUTTER_SYSTEM_INCLUDE_DIRECTORIES})
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
# Configure build options.
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE "Debug" CACHE
STRING "Flutter build mode" FORCE)
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
"Debug" "Profile" "Release")
endif()
# Configure build option to target backend.
if (NOT FLUTTER_TARGET_BACKEND_TYPE)
set(FLUTTER_TARGET_BACKEND_TYPE "wayland" CACHE
STRING "Flutter target backend type" FORCE)
set_property(CACHE FLUTTER_TARGET_BACKEND_TYPE PROPERTY STRINGS
"wayland" "gbm" "eglstream" "x11")
endif()
# Compilation settings that should be applied to most targets.
function(APPLY_STANDARD_SETTINGS TARGET)
target_compile_features(${TARGET} PUBLIC cxx_std_17)
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
endfunction()
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
# Flutter library and tool build rules.
add_subdirectory(${FLUTTER_MANAGED_DIR})
# Application build
add_subdirectory("runner")
# Generated plugin build rules, which manage building the plugins and adding
# them to the application.
include(flutter/generated_plugins.cmake)
# === Installation ===
# By default, "installing" just makes a relocatable bundle in the build
# directory.
set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
endif()
# Start with a clean build bundle directory every time.
install(CODE "
file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
" COMPONENT Runtime)
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
COMPONENT Runtime)
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
COMPONENT Runtime)
install(FILES "${FLUTTER_LIBRARY}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
install(FILES "${FLUTTER_EMBEDDER_LIBRARY}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
if(PLUGIN_BUNDLED_LIBRARIES)
install(FILES "${PLUGIN_BUNDLED_LIBRARIES}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endif()
# Fully re-copy the assets directory on each build to avoid having stale files
# from a previous install.
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
install(CODE "
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
" COMPONENT Runtime)
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
# Install the AOT library on non-Debug builds only.
if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endif()

View File

@ -0,0 +1,108 @@
cmake_minimum_required(VERSION 3.15)
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
# Configuration provided via flutter tool.
include(${EPHEMERAL_DIR}/generated_config.cmake)
set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper")
# Serves the same purpose as list(TRANSFORM ... PREPEND ...),
# which isn't available in 3.10.
function(list_prepend LIST_NAME PREFIX)
set(NEW_LIST "")
foreach(element ${${LIST_NAME}})
list(APPEND NEW_LIST "${PREFIX}${element}")
endforeach(element)
set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE)
endfunction()
# === Flutter Library ===
# System-level dependencies.
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_engine.so")
if(FLUTTER_TARGET_BACKEND_TYPE MATCHES "gbm")
set(FLUTTER_EMBEDDER_LIBRARY "${EPHEMERAL_DIR}/libflutter_elinux_gbm.so")
elseif(FLUTTER_TARGET_BACKEND_TYPE MATCHES "eglstream")
set(FLUTTER_EMBEDDER_LIBRARY "${EPHEMERAL_DIR}/libflutter_elinux_eglstream.so")
elseif(FLUTTER_TARGET_BACKEND_TYPE MATCHES "x11")
set(FLUTTER_EMBEDDER_LIBRARY "${EPHEMERAL_DIR}/libflutter_elinux_x11.so")
else()
set(FLUTTER_EMBEDDER_LIBRARY "${EPHEMERAL_DIR}/libflutter_elinux_wayland.so")
endif()
# Published to parent scope for install step.
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
set(FLUTTER_EMBEDDER_LIBRARY ${FLUTTER_EMBEDDER_LIBRARY} PARENT_SCOPE)
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/elinux/" PARENT_SCOPE)
set(AOT_LIBRARY "${EPHEMERAL_DIR}/libapp.so" PARENT_SCOPE)
list(APPEND FLUTTER_LIBRARY_HEADERS
"flutter_export.h"
"flutter_plugin_registrar.h"
"flutter_messenger.h"
"flutter_texture_registrar.h"
"flutter_elinux.h"
"flutter_platform_views.h"
)
list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/")
add_library(flutter INTERFACE)
target_include_directories(flutter INTERFACE
"${EPHEMERAL_DIR}"
)
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
target_link_libraries(flutter INTERFACE "${FLUTTER_EMBEDDER_LIBRARY}")
add_dependencies(flutter flutter_assemble)
# === Wrapper ===
list(APPEND CPP_WRAPPER_SOURCES_CORE
"core_implementations.cc"
"standard_codec.cc"
)
list_prepend(CPP_WRAPPER_SOURCES_CORE "${WRAPPER_ROOT}/")
list(APPEND CPP_WRAPPER_SOURCES_PLUGIN
"plugin_registrar.cc"
)
list_prepend(CPP_WRAPPER_SOURCES_PLUGIN "${WRAPPER_ROOT}/")
list(APPEND CPP_WRAPPER_SOURCES_APP
"flutter_engine.cc"
"flutter_view_controller.cc"
)
list_prepend(CPP_WRAPPER_SOURCES_APP "${WRAPPER_ROOT}/")
# Wrapper sources needed for a plugin.
add_library(flutter_wrapper_plugin STATIC
${CPP_WRAPPER_SOURCES_CORE}
${CPP_WRAPPER_SOURCES_PLUGIN}
)
apply_standard_settings(flutter_wrapper_plugin)
set_target_properties(flutter_wrapper_plugin PROPERTIES
POSITION_INDEPENDENT_CODE ON)
set_target_properties(flutter_wrapper_plugin PROPERTIES
CXX_VISIBILITY_PRESET hidden)
target_link_libraries(flutter_wrapper_plugin PUBLIC flutter)
target_include_directories(flutter_wrapper_plugin PUBLIC
"${WRAPPER_ROOT}/include"
)
add_dependencies(flutter_wrapper_plugin flutter_assemble)
# Wrapper sources needed for the runner.
add_library(flutter_wrapper_app STATIC
${CPP_WRAPPER_SOURCES_CORE}
${CPP_WRAPPER_SOURCES_APP}
)
apply_standard_settings(flutter_wrapper_app)
target_link_libraries(flutter_wrapper_app PUBLIC flutter)
target_include_directories(flutter_wrapper_app PUBLIC
"${WRAPPER_ROOT}/include"
)
add_dependencies(flutter_wrapper_app flutter_assemble)
add_custom_target(flutter_assemble DEPENDS
"${FLUTTER_LIBRARY}"
"${FLUTTER_EMBEDDER_LIBRARY}"
${FLUTTER_LIBRARY_HEADERS}
${CPP_WRAPPER_SOURCES_CORE}
${CPP_WRAPPER_SOURCES_PLUGIN}
${CPP_WRAPPER_SOURCES_APP}
)

View File

@ -0,0 +1,16 @@
#
# Generated file, do not edit.
#
list(APPEND FLUTTER_PLUGIN_LIST
audioplayers_elinux
)
set(PLUGIN_BUNDLED_LIBRARIES)
foreach(plugin ${FLUTTER_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/elinux plugins/${plugin})
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
endforeach(plugin)

View File

@ -0,0 +1,23 @@
cmake_minimum_required(VERSION 3.15)
project(runner LANGUAGES CXX)
if(FLUTTER_TARGET_BACKEND_TYPE MATCHES "gbm")
add_definitions(-DFLUTTER_TARGET_BACKEND_GBM)
elseif(FLUTTER_TARGET_BACKEND_TYPE MATCHES "eglstream")
add_definitions(-DFLUTTER_TARGET_BACKEND_EGLSTREAM)
elseif(FLUTTER_TARGET_BACKEND_TYPE MATCHES "x11")
add_definitions(-DFLUTTER_TARGET_BACKEND_X11)
else()
add_definitions(-DFLUTTER_TARGET_BACKEND_WAYLAND)
endif()
add_executable(${BINARY_NAME}
"flutter_window.cc"
"main.cc"
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
)
apply_standard_settings(${BINARY_NAME})
target_link_libraries(${BINARY_NAME} PRIVATE flutter)
target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
add_dependencies(${BINARY_NAME} flutter_assemble)

View File

@ -0,0 +1,402 @@
// Copyright 2022 Sony Corporation. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef COMMAND_OPTIONS_
#define COMMAND_OPTIONS_
#include <iostream>
#include <memory>
#include <sstream>
#include <stdexcept>
#include <string>
#include <unordered_map>
#include <vector>
namespace commandline {
namespace {
constexpr char kOptionStyleNormal[] = "--";
constexpr char kOptionStyleShort[] = "-";
constexpr char kOptionValueForHelpMessage[] = "=<value>";
} // namespace
class Exception : public std::exception {
public:
Exception(const std::string& msg) : msg_(msg) {}
~Exception() throw() {}
const char* what() const throw() { return msg_.c_str(); }
private:
std::string msg_;
};
class CommandOptions {
public:
CommandOptions() = default;
~CommandOptions() = default;
void AddWithoutValue(const std::string& name,
const std::string& short_name,
const std::string& description,
bool required) {
Add<std::string, ReaderString>(name, short_name, description, "",
ReaderString(), required, false);
}
void AddInt(const std::string& name,
const std::string& short_name,
const std::string& description,
const int& default_value,
bool required) {
Add<int, ReaderInt>(name, short_name, description, default_value,
ReaderInt(), required, true);
}
void AddDouble(const std::string& name,
const std::string& short_name,
const std::string& description,
const double& default_value,
bool required) {
Add<double, ReaderDouble>(name, short_name, description, default_value,
ReaderDouble(), required, true);
}
void AddString(const std::string& name,
const std::string& short_name,
const std::string& description,
const std::string& default_value,
bool required) {
Add<std::string, ReaderString>(name, short_name, description, default_value,
ReaderString(), required, true);
}
template <typename T, typename F>
void Add(const std::string& name,
const std::string& short_name,
const std::string& description,
const T default_value,
F reader = F(),
bool required = true,
bool required_value = true) {
if (options_.find(name) != options_.end()) {
std::cerr << "Already registered option: " << name << std::endl;
return;
}
if (lut_short_options_.find(short_name) != lut_short_options_.end()) {
std::cerr << short_name << "is already registered" << std::endl;
return;
}
lut_short_options_[short_name] = name;
options_[name] = std::make_unique<OptionValueReader<T, F>>(
name, short_name, description, default_value, reader, required,
required_value);
// register to show help message.
registration_order_options_.push_back(options_[name].get());
}
bool Exist(const std::string& name) {
auto itr = options_.find(name);
return itr != options_.end() && itr->second->HasValue();
}
template <typename T>
const T& GetValue(const std::string& name) {
auto itr = options_.find(name);
if (itr == options_.end()) {
throw Exception("Not found: " + name);
}
auto* option_value = dynamic_cast<const OptionValue<T>*>(itr->second.get());
if (!option_value) {
throw Exception("Type mismatch: " + name);
}
return option_value->GetValue();
}
bool Parse(int argc, const char* const* argv) {
if (argc < 1) {
errors_.push_back("No options");
return false;
}
command_name_ = argv[0];
for (auto i = 1; i < argc; i++) {
const std::string arg(argv[i]);
// normal options: e.g. --bundle=/data/sample/bundle --fullscreen
if (arg.length() > 2 &&
arg.substr(0, 2).compare(kOptionStyleNormal) == 0) {
const size_t option_value_len = arg.find("=") != std::string::npos
? (arg.length() - arg.find("="))
: 0;
const bool has_value = option_value_len != 0;
std::string option_name =
arg.substr(2, arg.length() - 2 - option_value_len);
if (options_.find(option_name) == options_.end()) {
errors_.push_back("Not found option: " + option_name);
continue;
}
if (!has_value && options_[option_name]->IsRequiredValue()) {
errors_.push_back(option_name + " requres an option value");
continue;
}
if (has_value && !options_[option_name]->IsRequiredValue()) {
errors_.push_back(option_name + " doesn't requres an option value");
continue;
}
if (has_value) {
SetOptionValue(option_name, arg.substr(arg.find("=") + 1));
} else {
SetOption(option_name);
}
}
// short options: e.g. -f /foo/file.txt -h 640 -abc
else if (arg.length() > 1 &&
arg.substr(0, 1).compare(kOptionStyleShort) == 0) {
for (size_t j = 1; j < arg.length(); j++) {
const std::string option_name{argv[i][j]};
if (lut_short_options_.find(option_name) ==
lut_short_options_.end()) {
errors_.push_back("Not found short option: " + option_name);
break;
}
if (j == arg.length() - 1 &&
options_[lut_short_options_[option_name]]->IsRequiredValue()) {
if (i == argc - 1) {
errors_.push_back("Invalid format option: " + option_name);
break;
}
SetOptionValue(lut_short_options_[option_name], argv[++i]);
} else {
SetOption(lut_short_options_[option_name]);
}
}
} else {
errors_.push_back("Invalid format option: " + arg);
}
}
for (size_t i = 0; i < registration_order_options_.size(); i++) {
if (registration_order_options_[i]->IsRequired() &&
!registration_order_options_[i]->HasValue()) {
errors_.push_back(
std::string(registration_order_options_[i]->GetName()) +
" option is mandatory.");
}
}
return errors_.size() == 0;
}
std::string GetError() { return errors_.size() > 0 ? errors_[0] : ""; }
std::vector<std::string>& GetErrors() { return errors_; }
std::string ShowHelp() {
std::ostringstream ostream;
ostream << "Usage: " << command_name_ << " ";
for (size_t i = 0; i < registration_order_options_.size(); i++) {
if (registration_order_options_[i]->IsRequired()) {
ostream << registration_order_options_[i]->GetHelpShortMessage() << " ";
}
}
ostream << std::endl;
ostream << "Global options:" << std::endl;
size_t max_name_len = 0;
for (size_t i = 0; i < registration_order_options_.size(); i++) {
max_name_len = std::max(
max_name_len, registration_order_options_[i]->GetName().length());
}
for (size_t i = 0; i < registration_order_options_.size(); i++) {
if (!registration_order_options_[i]->GetShortName().empty()) {
ostream << kOptionStyleShort
<< registration_order_options_[i]->GetShortName() << ", ";
} else {
ostream << std::string(4, ' ');
}
size_t index_adjust = 0;
constexpr int kSpacerNum = 10;
auto need_value = registration_order_options_[i]->IsRequiredValue();
ostream << kOptionStyleNormal
<< registration_order_options_[i]->GetName();
if (need_value) {
ostream << kOptionValueForHelpMessage;
index_adjust += std::string(kOptionValueForHelpMessage).length();
}
ostream << std::string(
max_name_len + kSpacerNum - index_adjust -
registration_order_options_[i]->GetName().length(),
' ');
ostream << registration_order_options_[i]->GetDescription() << std::endl;
}
return ostream.str();
}
private:
struct ReaderInt {
int operator()(const std::string& value) { return std::stoi(value); }
};
struct ReaderString {
std::string operator()(const std::string& value) { return value; }
};
struct ReaderDouble {
double operator()(const std::string& value) { return std::stod(value); }
};
class Option {
public:
Option(const std::string& name,
const std::string& short_name,
const std::string& description,
bool required,
bool required_value)
: name_(name),
short_name_(short_name),
description_(description),
is_required_(required),
is_required_value_(required_value),
value_set_(false){};
virtual ~Option() = default;
const std::string& GetName() const { return name_; };
const std::string& GetShortName() const { return short_name_; };
const std::string& GetDescription() const { return description_; };
const std::string GetHelpShortMessage() const {
std::string message = kOptionStyleNormal + name_;
if (is_required_value_) {
message += kOptionValueForHelpMessage;
}
return message;
}
bool IsRequired() const { return is_required_; };
bool IsRequiredValue() const { return is_required_value_; };
void Set() { value_set_ = true; };
virtual bool SetValue(const std::string& value) = 0;
virtual bool HasValue() const = 0;
protected:
std::string name_;
std::string short_name_;
std::string description_;
bool is_required_;
bool is_required_value_;
bool value_set_;
};
template <typename T>
class OptionValue : public Option {
public:
OptionValue(const std::string& name,
const std::string& short_name,
const std::string& description,
const T& default_value,
bool required,
bool required_value)
: Option(name, short_name, description, required, required_value),
default_value_(default_value),
value_(default_value){};
virtual ~OptionValue() = default;
bool SetValue(const std::string& value) {
value_ = Read(value);
value_set_ = true;
return true;
}
bool HasValue() const { return value_set_; }
const T& GetValue() const { return value_; }
protected:
virtual T Read(const std::string& s) = 0;
T default_value_;
T value_;
};
template <typename T, typename F>
class OptionValueReader : public OptionValue<T> {
public:
OptionValueReader(const std::string& name,
const std::string& short_name,
const std::string& description,
const T default_value,
F reader,
bool required,
bool required_value)
: OptionValue<T>(name,
short_name,
description,
default_value,
required,
required_value),
reader_(reader) {}
~OptionValueReader() = default;
private:
T Read(const std::string& value) { return reader_(value); }
F reader_;
};
bool SetOption(const std::string& name) {
auto itr = options_.find(name);
if (itr == options_.end()) {
errors_.push_back("Unknown option: " + name);
return false;
}
itr->second->Set();
return true;
}
bool SetOptionValue(const std::string& name, const std::string& value) {
auto itr = options_.find(name);
if (itr == options_.end()) {
errors_.push_back("Unknown option: " + name);
return false;
}
if (!itr->second->SetValue(value)) {
errors_.push_back("Invalid option value: " + name + " = " + value);
return false;
}
return true;
}
std::string command_name_;
std::unordered_map<std::string, std::unique_ptr<Option>> options_;
std::unordered_map<std::string, std::string> lut_short_options_;
std::vector<Option*> registration_order_options_;
std::vector<std::string> errors_;
};
} // namespace commandline
#endif // COMMAND_OPTIONS_

View File

@ -0,0 +1,203 @@
// Copyright 2021 Sony Corporation. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef FLUTTER_EMBEDDER_OPTIONS_
#define FLUTTER_EMBEDDER_OPTIONS_
#include <flutter/flutter_view_controller.h>
#include <string>
#include "command_options.h"
class FlutterEmbedderOptions {
public:
FlutterEmbedderOptions() {
options_.AddString("bundle", "b", "Path to Flutter project bundle",
"./bundle", true);
options_.AddWithoutValue("no-cursor", "n", "No mouse cursor/pointer",
false);
options_.AddInt("rotation", "r",
"Window rotation(degree) [0(default)|90|180|270]", 0,
false);
options_.AddDouble("text-scaling-factor", "x", "Text scaling factor", 1.0,
false);
options_.AddWithoutValue("enable-high-contrast", "i",
"Request that UI be rendered with darker colors.",
false);
options_.AddDouble("force-scale-factor", "s",
"Force a scale factor instead using default value", 1.0,
false);
options_.AddWithoutValue(
"async-vblank", "v",
"Don't sync to compositor redraw/vblank (eglSwapInterval 0)", false);
#if defined(FLUTTER_TARGET_BACKEND_GBM) || \
defined(FLUTTER_TARGET_BACKEND_EGLSTREAM)
// no more options.
#elif defined(FLUTTER_TARGET_BACKEND_X11)
options_.AddString("title", "t", "Window title", "Flutter", false);
options_.AddWithoutValue("fullscreen", "f", "Always full-screen display",
false);
options_.AddInt("width", "w", "Window width", 1280, false);
options_.AddInt("height", "h", "Window height", 720, false);
#else // FLUTTER_TARGET_BACKEND_WAYLAND
options_.AddString("title", "t", "Window title", "Flutter", false);
options_.AddString("app-id", "a", "XDG App ID", "dev.flutter.elinux",
false);
options_.AddWithoutValue("onscreen-keyboard", "k",
"Enable on-screen keyboard", false);
options_.AddWithoutValue("window-decoration", "d",
"Enable window decorations", false);
options_.AddWithoutValue("fullscreen", "f", "Always full-screen display",
false);
options_.AddInt("width", "w", "Window width", 1280, false);
options_.AddInt("height", "h", "Window height", 720, false);
#endif
}
~FlutterEmbedderOptions() = default;
bool Parse(int argc, char** argv) {
if (!options_.Parse(argc, argv)) {
std::cerr << options_.GetError() << std::endl;
std::cout << options_.ShowHelp();
return false;
}
bundle_path_ = options_.GetValue<std::string>("bundle");
use_mouse_cursor_ = !options_.Exist("no-cursor");
if (options_.Exist("rotation")) {
switch (options_.GetValue<int>("rotation")) {
case 90:
window_view_rotation_ =
flutter::FlutterViewController::ViewRotation::kRotation_90;
break;
case 180:
window_view_rotation_ =
flutter::FlutterViewController::ViewRotation::kRotation_180;
break;
case 270:
window_view_rotation_ =
flutter::FlutterViewController::ViewRotation::kRotation_270;
break;
default:
window_view_rotation_ =
flutter::FlutterViewController::ViewRotation::kRotation_0;
break;
}
}
text_scale_factor_ = options_.GetValue<double>("text-scaling-factor");
enable_high_contrast_ = options_.Exist("enable-high-contrast");
if (options_.Exist("force-scale-factor")) {
is_force_scale_factor_ = true;
scale_factor_ = options_.GetValue<double>("force-scale-factor");
} else {
is_force_scale_factor_ = false;
scale_factor_ = 1.0;
}
enable_vsync_ = !options_.Exist("async-vblank");
#if defined(FLUTTER_TARGET_BACKEND_GBM) || \
defined(FLUTTER_TARGET_BACKEND_EGLSTREAM)
use_onscreen_keyboard_ = false;
use_window_decoration_ = false;
window_view_mode_ = flutter::FlutterViewController::ViewMode::kFullscreen;
#elif defined(FLUTTER_TARGET_BACKEND_X11)
use_onscreen_keyboard_ = false;
use_window_decoration_ = false;
window_title_ = options_.GetValue<std::string>("title");
window_view_mode_ =
options_.Exist("fullscreen")
? flutter::FlutterViewController::ViewMode::kFullscreen
: flutter::FlutterViewController::ViewMode::kNormal;
window_width_ = options_.GetValue<int>("width");
window_height_ = options_.GetValue<int>("height");
#else // FLUTTER_TARGET_BACKEND_WAYLAND
window_title_ = options_.GetValue<std::string>("title");
window_app_id_ = options_.GetValue<std::string>("app-id");
use_onscreen_keyboard_ = options_.Exist("onscreen-keyboard");
use_window_decoration_ = options_.Exist("window-decoration");
window_view_mode_ =
options_.Exist("fullscreen")
? flutter::FlutterViewController::ViewMode::kFullscreen
: flutter::FlutterViewController::ViewMode::kNormal;
window_width_ = options_.GetValue<int>("width");
window_height_ = options_.GetValue<int>("height");
#endif
return true;
}
std::string BundlePath() const {
return bundle_path_;
}
std::string WindowTitle() const {
return window_title_;
}
std::string WindowAppID() const {
return window_app_id_;
}
bool IsUseMouseCursor() const {
return use_mouse_cursor_;
}
bool IsUseOnscreenKeyboard() const {
return use_onscreen_keyboard_;
}
bool IsUseWindowDecoraation() const {
return use_window_decoration_;
}
flutter::FlutterViewController::ViewMode WindowViewMode() const {
return window_view_mode_;
}
int WindowWidth() const {
return window_width_;
}
int WindowHeight() const {
return window_height_;
}
flutter::FlutterViewController::ViewRotation WindowRotation() const {
return window_view_rotation_;
}
double TextScaleFactor() const {
return text_scale_factor_;
}
bool EnableHighContrast() const {
return enable_high_contrast_;
}
bool IsForceScaleFactor() const {
return is_force_scale_factor_;
}
double ScaleFactor() const {
return scale_factor_;
}
bool EnableVsync() const {
return enable_vsync_;
}
private:
commandline::CommandOptions options_;
std::string bundle_path_;
std::string window_title_;
std::string window_app_id_;
bool use_mouse_cursor_ = true;
bool use_onscreen_keyboard_ = false;
bool use_window_decoration_ = false;
flutter::FlutterViewController::ViewMode window_view_mode_ =
flutter::FlutterViewController::ViewMode::kNormal;
int window_width_ = 1280;
int window_height_ = 720;
flutter::FlutterViewController::ViewRotation window_view_rotation_ =
flutter::FlutterViewController::ViewRotation::kRotation_0;
bool is_force_scale_factor_;
double scale_factor_;
double text_scale_factor_;
bool enable_high_contrast_;
bool enable_vsync_;
};
#endif // FLUTTER_EMBEDDER_OPTIONS_

View File

@ -0,0 +1,79 @@
// Copyright 2021 Sony Corporation. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "flutter_window.h"
#include <chrono>
#include <cmath>
#include <iostream>
#include <thread>
#include "flutter/generated_plugin_registrant.h"
FlutterWindow::FlutterWindow(
const flutter::FlutterViewController::ViewProperties view_properties,
const flutter::DartProject project)
: view_properties_(view_properties), project_(project) {}
bool FlutterWindow::OnCreate() {
flutter_view_controller_ = std::make_unique<flutter::FlutterViewController>(
view_properties_, project_);
// Ensure that basic setup of the controller was successful.
if (!flutter_view_controller_->engine() ||
!flutter_view_controller_->view()) {
return false;
}
// Register Flutter plugins.
RegisterPlugins(flutter_view_controller_->engine());
return true;
}
void FlutterWindow::OnDestroy() {
if (flutter_view_controller_) {
flutter_view_controller_ = nullptr;
}
}
void FlutterWindow::Run() {
// Main loop.
auto next_flutter_event_time =
std::chrono::steady_clock::time_point::clock::now();
while (flutter_view_controller_->view()->DispatchEvent()) {
// Wait until the next event.
{
auto wait_duration =
std::max(std::chrono::nanoseconds(0),
next_flutter_event_time -
std::chrono::steady_clock::time_point::clock::now());
std::this_thread::sleep_for(
std::chrono::duration_cast<std::chrono::milliseconds>(wait_duration));
}
// Processes any pending events in the Flutter engine, and returns the
// number of nanoseconds until the next scheduled event (or max, if none).
auto wait_duration = flutter_view_controller_->engine()->ProcessMessages();
{
auto next_event_time = std::chrono::steady_clock::time_point::max();
if (wait_duration != std::chrono::nanoseconds::max()) {
next_event_time =
std::min(next_event_time,
std::chrono::steady_clock::time_point::clock::now() +
wait_duration);
} else {
// Wait for the next frame if no events.
auto frame_rate = flutter_view_controller_->view()->GetFrameRate();
next_event_time = std::min(
next_event_time,
std::chrono::steady_clock::time_point::clock::now() +
std::chrono::milliseconds(
static_cast<int>(std::trunc(1000000.0 / frame_rate))));
}
next_flutter_event_time =
std::max(next_flutter_event_time, next_event_time);
}
}
}

View File

@ -0,0 +1,34 @@
// Copyright 2021 Sony Corporation. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef FLUTTER_WINDOW_
#define FLUTTER_WINDOW_
#include <flutter/dart_project.h>
#include <flutter/flutter_view_controller.h>
#include <memory>
class FlutterWindow {
public:
explicit FlutterWindow(
const flutter::FlutterViewController::ViewProperties view_properties,
const flutter::DartProject project);
~FlutterWindow() = default;
// Prevent copying.
FlutterWindow(FlutterWindow const&) = delete;
FlutterWindow& operator=(FlutterWindow const&) = delete;
bool OnCreate();
void OnDestroy();
void Run();
private:
flutter::FlutterViewController::ViewProperties view_properties_;
flutter::DartProject project_;
std::unique_ptr<flutter::FlutterViewController> flutter_view_controller_;
};
#endif // FLUTTER_WINDOW_

View File

@ -0,0 +1,53 @@
// Copyright 2021 Sony Corporation. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <flutter/dart_project.h>
#include <flutter/flutter_view_controller.h>
#include <iostream>
#include <memory>
#include <string>
#include "flutter_embedder_options.h"
#include "flutter_window.h"
int main(int argc, char** argv) {
FlutterEmbedderOptions options;
if (!options.Parse(argc, argv)) {
return 0;
}
// Creates the Flutter project.
const auto bundle_path = options.BundlePath();
const std::wstring fl_path(bundle_path.begin(), bundle_path.end());
flutter::DartProject project(fl_path);
auto command_line_arguments = std::vector<std::string>();
project.set_dart_entrypoint_arguments(std::move(command_line_arguments));
flutter::FlutterViewController::ViewProperties view_properties = {};
view_properties.width = options.WindowWidth();
view_properties.height = options.WindowHeight();
view_properties.view_mode = options.WindowViewMode();
view_properties.view_rotation = options.WindowRotation();
view_properties.title = options.WindowTitle();
view_properties.app_id = options.WindowAppID();
view_properties.use_mouse_cursor = options.IsUseMouseCursor();
view_properties.use_onscreen_keyboard = options.IsUseOnscreenKeyboard();
view_properties.use_window_decoration = options.IsUseWindowDecoraation();
view_properties.text_scale_factor = options.TextScaleFactor();
view_properties.enable_high_contrast = options.EnableHighContrast();
view_properties.force_scale_factor = options.IsForceScaleFactor();
view_properties.scale_factor = options.ScaleFactor();
view_properties.enable_vsync = options.EnableVsync();
// The Flutter instance hosted by this window.
FlutterWindow window(view_properties, project);
if (!window.OnCreate()) {
return 0;
}
window.Run();
window.OnDestroy();
return 0;
}

View File

@ -0,0 +1,24 @@
import 'package:flutter/material.dart';
class Btn extends StatelessWidget {
final String txt;
final VoidCallback onPressed;
const Btn({
required this.txt,
required this.onPressed,
super.key,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(4),
child: ElevatedButton(
style: ElevatedButton.styleFrom(minimumSize: const Size(48, 36)),
onPressed: onPressed,
child: Text(txt),
),
);
}
}

View File

@ -0,0 +1,23 @@
import 'package:flutter/material.dart';
class Cbx extends StatelessWidget {
final String label;
final bool value;
final void Function({required bool? value}) update;
const Cbx(
this.label,
this.update, {
required this.value,
super.key,
});
@override
Widget build(BuildContext context) {
return CheckboxListTile(
title: Text(label),
value: value,
onChanged: (v) => update(value: v),
);
}
}

View File

@ -0,0 +1,48 @@
import 'package:audioplayers_elinux_example/components/btn.dart';
import 'package:flutter/material.dart';
class SimpleDlg extends StatelessWidget {
final String message;
final String action;
const SimpleDlg({
required this.message,
required this.action,
super.key,
});
@override
Widget build(BuildContext context) {
return Dlg(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(message),
Btn(
txt: action,
onPressed: Navigator.of(context).pop,
),
],
),
);
}
}
class Dlg extends StatelessWidget {
final Widget child;
const Dlg({
required this.child,
super.key,
});
@override
Widget build(BuildContext context) {
return Dialog(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: child,
),
);
}
}

View File

@ -0,0 +1,60 @@
import 'package:flutter/material.dart';
class LabeledDropDown<T> extends StatelessWidget {
final String label;
final Map<T, String> options;
final T selected;
final void Function(T?) onChange;
const LabeledDropDown({
required this.label,
required this.options,
required this.selected,
required this.onChange,
super.key,
});
@override
Widget build(BuildContext context) {
return ListTile(
title: Text(label),
trailing: CustomDropDown<T>(
options: options,
selected: selected,
onChange: onChange,
),
);
}
}
class CustomDropDown<T> extends StatelessWidget {
final Map<T, String> options;
final T selected;
final void Function(T?) onChange;
final bool isExpanded;
const CustomDropDown({
required this.options,
required this.selected,
required this.onChange,
this.isExpanded = false,
super.key,
});
@override
Widget build(BuildContext context) {
return DropdownButton<T>(
isExpanded: isExpanded,
value: selected,
onChanged: onChange,
items: options.entries
.map<DropdownMenuItem<T>>(
(entry) => DropdownMenuItem<T>(
value: entry.key,
child: Text(entry.value),
),
)
.toList(),
);
}
}

View File

@ -0,0 +1,26 @@
import 'package:flutter/material.dart';
class WrappedListTile extends StatelessWidget {
final List<Widget> children;
final Widget? leading;
final Widget? trailing;
const WrappedListTile({
required this.children,
this.leading,
this.trailing,
super.key,
});
@override
Widget build(BuildContext context) {
return ListTile(
title: Wrap(
alignment: WrapAlignment.end,
children: children,
),
leading: leading,
trailing: trailing,
);
}
}

View File

@ -0,0 +1,16 @@
import 'package:flutter/material.dart';
class Pad extends StatelessWidget {
final double width;
final double height;
const Pad({super.key, this.width = 0, this.height = 0});
@override
Widget build(BuildContext context) {
return Container(
width: width,
height: height,
);
}
}

View File

@ -0,0 +1,178 @@
import 'dart:async';
import 'package:audioplayers/audioplayers.dart';
import 'package:flutter/material.dart';
// This code is also used in the example.md. Please keep it up to date.
class PlayerWidget extends StatefulWidget {
final AudioPlayer player;
const PlayerWidget({
required this.player,
super.key,
});
@override
State<StatefulWidget> createState() {
return _PlayerWidgetState();
}
}
class _PlayerWidgetState extends State<PlayerWidget> {
PlayerState? _playerState;
Duration? _duration;
Duration? _position;
StreamSubscription? _durationSubscription;
StreamSubscription? _positionSubscription;
StreamSubscription? _playerCompleteSubscription;
StreamSubscription? _playerStateChangeSubscription;
bool get _isPlaying => _playerState == PlayerState.playing;
bool get _isPaused => _playerState == PlayerState.paused;
String get _durationText => _duration?.toString().split('.').first ?? '';
String get _positionText => _position?.toString().split('.').first ?? '';
AudioPlayer get player => widget.player;
@override
void initState() {
super.initState();
// Use initial values from player
_playerState = player.state;
player.getDuration().then(
(value) => setState(() {
_duration = value;
}),
);
player.getCurrentPosition().then(
(value) => setState(() {
_position = value;
}),
);
_initStreams();
}
@override
void setState(VoidCallback fn) {
// Subscriptions only can be closed asynchronously,
// therefore events can occur after widget has been disposed.
if (mounted) {
super.setState(fn);
}
}
@override
void dispose() {
_durationSubscription?.cancel();
_positionSubscription?.cancel();
_playerCompleteSubscription?.cancel();
_playerStateChangeSubscription?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
final color = Theme.of(context).primaryColor;
return Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
key: const Key('play_button'),
onPressed: _isPlaying ? null : _play,
iconSize: 48.0,
icon: const Icon(Icons.play_arrow),
color: color,
),
IconButton(
key: const Key('pause_button'),
onPressed: _isPlaying ? _pause : null,
iconSize: 48.0,
icon: const Icon(Icons.pause),
color: color,
),
IconButton(
key: const Key('stop_button'),
onPressed: _isPlaying || _isPaused ? _stop : null,
iconSize: 48.0,
icon: const Icon(Icons.stop),
color: color,
),
],
),
Slider(
onChanged: (value) {
final duration = _duration;
if (duration == null) {
return;
}
final position = value * duration.inMilliseconds;
player.seek(Duration(milliseconds: position.round()));
},
value: (_position != null &&
_duration != null &&
_position!.inMilliseconds > 0 &&
_position!.inMilliseconds < _duration!.inMilliseconds)
? _position!.inMilliseconds / _duration!.inMilliseconds
: 0.0,
),
Text(
_position != null
? '$_positionText / $_durationText'
: _duration != null
? _durationText
: '',
style: const TextStyle(fontSize: 16.0),
),
],
);
}
void _initStreams() {
_durationSubscription = player.onDurationChanged.listen((duration) {
setState(() => _duration = duration);
});
_positionSubscription = player.onPositionChanged.listen(
(p) => setState(() => _position = p),
);
_playerCompleteSubscription = player.onPlayerComplete.listen((event) {
setState(() {
_playerState = PlayerState.stopped;
_position = Duration.zero;
});
});
_playerStateChangeSubscription =
player.onPlayerStateChanged.listen((state) {
setState(() {
_playerState = state;
});
});
}
Future<void> _play() async {
await player.resume();
setState(() => _playerState = PlayerState.playing);
}
Future<void> _pause() async {
await player.pause();
setState(() => _playerState = PlayerState.paused);
}
Future<void> _stop() async {
await player.stop();
setState(() {
_playerState = PlayerState.stopped;
_position = Duration.zero;
});
}
}

View File

@ -0,0 +1,104 @@
import 'package:audioplayers/audioplayers.dart';
import 'package:audioplayers_elinux_example/utils.dart';
import 'package:flutter/material.dart';
class PropertiesWidget extends StatefulWidget {
final AudioPlayer player;
const PropertiesWidget({
required this.player,
super.key,
});
@override
State<PropertiesWidget> createState() => _PropertiesWidgetState();
}
class _PropertiesWidgetState extends State<PropertiesWidget> {
Future<void> refresh() async {
setState(() {});
}
@override
Widget build(BuildContext context) {
return Column(
children: [
ListTile(
title: const Text('Properties'),
trailing: ElevatedButton.icon(
icon: const Icon(Icons.refresh),
key: const Key('refreshButton'),
label: const Text('Refresh'),
onPressed: refresh,
),
),
ListTile(
title: FutureBuilder<Duration?>(
future: widget.player.getDuration(),
builder: (context, snap) {
return Text(
snap.data?.toString() ?? '-',
key: const Key('durationText'),
);
},
),
subtitle: const Text('Duration'),
leading: const Icon(Icons.timelapse),
),
ListTile(
title: FutureBuilder<Duration?>(
future: widget.player.getCurrentPosition(),
builder: (context, snap) {
return Text(
snap.data?.toString() ?? '-',
key: const Key('positionText'),
);
},
),
subtitle: const Text('Position'),
leading: const Icon(Icons.timer),
),
ListTile(
title: Text(
widget.player.state.toString(),
key: const Key('playerStateText'),
),
subtitle: const Text('State'),
leading: Icon(widget.player.state.getIcon()),
),
ListTile(
title: Text(
widget.player.source?.toString() ?? '-',
key: const Key('sourceText'),
),
subtitle: const Text('Source'),
leading: const Icon(Icons.audio_file),
),
ListTile(
title: Text(
widget.player.volume.toString(),
key: const Key('volumeText'),
),
subtitle: const Text('Volume'),
leading: const Icon(Icons.volume_up),
),
ListTile(
title: Text(
widget.player.balance.toString(),
key: const Key('balanceText'),
),
subtitle: const Text('Balance'),
leading: const Icon(Icons.balance),
),
ListTile(
title: Text(
widget.player.playbackRate.toString(),
key: const Key('playbackRateText'),
),
subtitle: const Text('Playback Rate'),
leading: const Icon(Icons.speed),
),
],
);
}
}

View File

@ -0,0 +1,94 @@
import 'dart:async';
import 'package:audioplayers/audioplayers.dart';
import 'package:audioplayers_elinux_example/utils.dart';
import 'package:flutter/material.dart';
class StreamWidget extends StatefulWidget {
final AudioPlayer player;
const StreamWidget({
required this.player,
super.key,
});
@override
State<StreamWidget> createState() => _StreamWidgetState();
}
class _StreamWidgetState extends State<StreamWidget> {
Duration? streamDuration;
Duration? streamPosition;
PlayerState? streamState;
late List<StreamSubscription> streams;
AudioPlayer get player => widget.player;
@override
void initState() {
super.initState();
// Use initial values from player
streamState = player.state;
player.getDuration().then((it) => setState(() => streamDuration = it));
player.getCurrentPosition().then(
(it) => setState(() => streamPosition = it),
);
streams = <StreamSubscription>[
player.onDurationChanged
.listen((it) => setState(() => streamDuration = it)),
player.onPlayerStateChanged
.listen((it) => setState(() => streamState = it)),
player.onPositionChanged
.listen((it) => setState(() => streamPosition = it)),
];
}
@override
void dispose() {
super.dispose();
streams.forEach((it) => it.cancel());
}
@override
void setState(VoidCallback fn) {
// Subscriptions only can be closed asynchronously,
// therefore events can occur after widget has been disposed.
if (mounted) {
super.setState(fn);
}
}
@override
Widget build(BuildContext context) {
return Column(
children: [
const ListTile(title: Text('Streams')),
ListTile(
title: Text(
streamDuration?.toString() ?? '-',
key: const Key('onDurationText'),
),
subtitle: const Text('Duration Stream'),
leading: const Icon(Icons.timelapse),
),
ListTile(
title: Text(
streamPosition?.toString() ?? '-',
key: const Key('onPositionText'),
),
subtitle: const Text('Position Stream'),
leading: const Icon(Icons.timer),
),
ListTile(
title: Text(
streamState?.toString() ?? '-',
key: const Key('onStateText'),
),
subtitle: const Text('State Stream'),
leading: Icon(streamState?.getIcon() ?? Icons.stop),
),
],
);
}
}

View File

@ -0,0 +1,28 @@
import 'package:flutter/material.dart';
class TabContent extends StatelessWidget {
final List<Widget> children;
const TabContent({
required this.children,
super.key,
});
@override
Widget build(BuildContext context) {
return Center(
child: Container(
alignment: Alignment.topCenter,
child: SingleChildScrollView(
controller: ScrollController(),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Column(
children: children,
),
),
),
),
);
}
}

View File

@ -0,0 +1,52 @@
import 'package:flutter/material.dart';
class Tabs extends StatelessWidget {
final List<TabData> tabs;
const Tabs({
required this.tabs,
super.key,
});
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: tabs.length,
child: Scaffold(
body: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TabBar(
labelColor: Colors.black,
tabs: tabs
.map(
(tData) => Tab(
key: tData.key != null ? Key(tData.key!) : null,
text: tData.label,
),
)
.toList(),
),
Expanded(
child: TabBarView(
children: tabs.map((tab) => tab.content).toList(),
),
),
],
),
),
);
}
}
class TabData {
final String? key;
final String label;
final Widget content;
TabData({
required this.label,
required this.content,
this.key,
});
}

View File

@ -0,0 +1,61 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
class Tgl extends StatelessWidget {
final Map<String, String> options;
final int selected;
final void Function(int) onChange;
const Tgl({
required this.options,
required this.selected,
required this.onChange,
super.key,
});
@override
Widget build(BuildContext context) {
return ToggleButtons(
isSelected: options.entries
.mapIndexed((index, element) => index == selected)
.toList(),
onPressed: onChange,
borderRadius: const BorderRadius.all(Radius.circular(8)),
selectedBorderColor: Theme.of(context).primaryColor,
children: options.entries
.map(
(entry) => Padding(
padding: const EdgeInsets.all(8),
child: Text(
entry.value,
key: Key(entry.key),
),
),
)
.toList(),
);
}
}
class EnumTgl<T extends Enum> extends StatelessWidget {
final Map<String, T> options;
final T selected;
final void Function(T) onChange;
const EnumTgl({
required this.options,
required this.selected,
required this.onChange,
super.key,
});
@override
Widget build(BuildContext context) {
final optionValues = options.values.toList();
return Tgl(
options: options.map((key, value) => MapEntry(key, value.name)),
selected: optionValues.indexOf(selected),
onChange: (it) => onChange(optionValues[it]),
);
}
}

View File

@ -0,0 +1,38 @@
import 'package:flutter/material.dart';
class TxtBox extends StatefulWidget {
final String value;
final void Function(String) onChange;
const TxtBox({
required this.value,
required this.onChange,
super.key,
});
@override
State<TxtBox> createState() => _TxtBoxState();
}
class _TxtBoxState extends State<TxtBox> {
late TextEditingController _controller;
@override
void initState() {
super.initState();
_controller = TextEditingController(
text: widget.value,
)..addListener(() => widget.onChange(_controller.text));
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return TextField(controller: _controller);
}
}

View File

@ -0,0 +1,208 @@
import 'dart:async';
import 'package:audioplayers/audioplayers.dart';
import 'package:audioplayers_elinux_example/components/tabs.dart';
import 'package:audioplayers_elinux_example/components/tgl.dart';
import 'package:audioplayers_elinux_example/tabs/audio_context.dart';
import 'package:audioplayers_elinux_example/tabs/controls.dart';
import 'package:audioplayers_elinux_example/tabs/logger.dart';
import 'package:audioplayers_elinux_example/tabs/sources.dart';
import 'package:audioplayers_elinux_example/tabs/streams.dart';
import 'package:audioplayers_elinux_example/utils.dart';
import 'package:flutter/material.dart';
const defaultPlayerCount = 4;
typedef OnError = void Function(Exception exception);
/// The app is deployed at: https://bluefireteam.github.io/audioplayers/
void main() {
runApp(const MaterialApp(home: _ExampleApp()));
}
class _ExampleApp extends StatefulWidget {
const _ExampleApp();
@override
_ExampleAppState createState() => _ExampleAppState();
}
class _ExampleAppState extends State<_ExampleApp> {
List<AudioPlayer> audioPlayers = List.generate(
defaultPlayerCount,
(_) => AudioPlayer()..setReleaseMode(ReleaseMode.stop),
);
int selectedPlayerIdx = 0;
AudioPlayer get selectedAudioPlayer => audioPlayers[selectedPlayerIdx];
List<StreamSubscription> streams = [];
@override
void initState() {
super.initState();
audioPlayers.asMap().forEach((index, player) {
streams.add(
player.onPlayerStateChanged.listen(
(it) {
switch (it) {
case PlayerState.stopped:
toast(
'Player stopped!',
textKey: Key('toast-player-stopped-$index'),
);
break;
case PlayerState.completed:
toast(
'Player complete!',
textKey: Key('toast-player-complete-$index'),
);
break;
default:
break;
}
},
),
);
streams.add(
player.onSeekComplete.listen(
(it) => toast(
'Seek complete!',
textKey: Key('toast-seek-complete-$index'),
),
),
);
});
}
@override
void dispose() {
streams.forEach((it) => it.cancel());
super.dispose();
}
void _handleAction(PopupAction value) {
switch (value) {
case PopupAction.add:
setState(() {
audioPlayers.add(AudioPlayer()..setReleaseMode(ReleaseMode.stop));
});
break;
case PopupAction.remove:
setState(() {
if (audioPlayers.isNotEmpty) {
selectedAudioPlayer.dispose();
audioPlayers.removeAt(selectedPlayerIdx);
}
// Adjust index to be in valid range
if (audioPlayers.isEmpty) {
selectedPlayerIdx = 0;
} else if (selectedPlayerIdx >= audioPlayers.length) {
selectedPlayerIdx = audioPlayers.length - 1;
}
});
break;
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('AudioPlayers example'),
actions: [
PopupMenuButton<PopupAction>(
onSelected: _handleAction,
itemBuilder: (BuildContext context) {
return PopupAction.values.map((PopupAction choice) {
return PopupMenuItem<PopupAction>(
value: choice,
child: Text(
choice == PopupAction.add
? 'Add player'
: 'Remove selected player',
),
);
}).toList();
},
),
],
),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Center(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Tgl(
key: const Key('playerTgl'),
options: [for (var i = 1; i <= audioPlayers.length; i++) i]
.asMap()
.map((key, val) => MapEntry('player-$key', 'P$val')),
selected: selectedPlayerIdx,
onChange: (v) => setState(() => selectedPlayerIdx = v),
),
),
),
),
Expanded(
child: audioPlayers.isEmpty
? const Text('No AudioPlayer available!')
: IndexedStack(
index: selectedPlayerIdx,
children: audioPlayers
.map(
(player) => Tabs(
key: GlobalObjectKey(player),
tabs: [
TabData(
key: 'sourcesTab',
label: 'Src',
content: SourcesTab(
player: player,
),
),
TabData(
key: 'controlsTab',
label: 'Ctrl',
content: ControlsTab(
player: player,
),
),
TabData(
key: 'streamsTab',
label: 'Stream',
content: StreamsTab(
player: player,
),
),
TabData(
key: 'audioContextTab',
label: 'Ctx',
content: AudioContextTab(
player: player,
),
),
TabData(
key: 'loggerTab',
label: 'Log',
content: LoggerTab(
player: player,
),
),
],
),
)
.toList(),
),
),
],
),
);
}
}
enum PopupAction {
add,
remove,
}

View File

@ -0,0 +1,246 @@
import 'package:audioplayers/audioplayers.dart';
import 'package:audioplayers_elinux_example/components/cbx.dart';
import 'package:audioplayers_elinux_example/components/drop_down.dart';
import 'package:audioplayers_elinux_example/components/tab_content.dart';
import 'package:audioplayers_elinux_example/components/tabs.dart';
import 'package:audioplayers_elinux_example/utils.dart';
import 'package:flutter/material.dart';
class AudioContextTab extends StatefulWidget {
final AudioPlayer player;
const AudioContextTab({
required this.player,
super.key,
});
@override
AudioContextTabState createState() => AudioContextTabState();
}
class AudioContextTabState extends State<AudioContextTab>
with AutomaticKeepAliveClientMixin<AudioContextTab> {
static GlobalAudioScope get _global => AudioPlayer.global;
AudioPlayer get player => widget.player;
/// Set config for all platforms
AudioContextConfig audioContextConfig = AudioContextConfig();
/// Set config for each platform individually
AudioContext audioContext = AudioContext();
@override
Widget build(BuildContext context) {
super.build(context);
return Column(
children: [
const ListTile(title: Text('Audio Context')),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
ElevatedButton.icon(
icon: const Icon(Icons.undo),
label: const Text('Reset'),
onPressed: () => updateConfig(AudioContextConfig()),
),
ElevatedButton.icon(
icon: const Icon(Icons.public),
label: const Text('Global'),
onPressed: () => _global.setAudioContext(audioContext),
),
ElevatedButton.icon(
icon: const Icon(Icons.looks_one),
label: const Text('Local'),
onPressed: () => player.setAudioContext(audioContext),
),
],
),
const SizedBox(height: 8),
Expanded(
child: Tabs(
tabs: [
TabData(
key: 'contextTab-genericFlags',
label: 'Generic Flags',
content: _genericTab(),
),
TabData(
key: 'contextTab-android',
label: 'Android',
content: _androidTab(),
),
TabData(
key: 'contextTab-ios',
label: 'iOS',
content: _iosTab(),
),
],
),
),
],
);
}
void updateConfig(AudioContextConfig newConfig) {
try {
final context = newConfig.build();
setState(() {
audioContextConfig = newConfig;
audioContext = context;
});
} on AssertionError catch (e) {
toast(e.message.toString());
}
}
void updateAudioContextAndroid(AudioContextAndroid contextAndroid) {
setState(() {
audioContext = audioContext.copy(android: contextAndroid);
});
}
void updateAudioContextIOS(AudioContextIOS Function() buildContextIOS) {
try {
final context = buildContextIOS();
setState(() {
audioContext = audioContext.copy(iOS: context);
});
} on AssertionError catch (e) {
toast(e.message.toString());
}
}
Widget _genericTab() {
return TabContent(
children: [
LabeledDropDown<AudioContextConfigRoute>(
label: 'Audio Route',
key: const Key('audioRoute'),
options: {for (final e in AudioContextConfigRoute.values) e: e.name},
selected: audioContextConfig.route,
onChange: (v) => updateConfig(
audioContextConfig.copy(route: v),
),
),
LabeledDropDown<AudioContextConfigFocus>(
label: 'Audio Focus',
key: const Key('audioFocus'),
options: {for (final e in AudioContextConfigFocus.values) e: e.name},
selected: audioContextConfig.focus,
onChange: (v) => updateConfig(
audioContextConfig.copy(focus: v),
),
),
Cbx(
'Respect Silence',
value: audioContextConfig.respectSilence,
({value}) =>
updateConfig(audioContextConfig.copy(respectSilence: value)),
),
Cbx(
'Stay Awake',
value: audioContextConfig.stayAwake,
({value}) => updateConfig(audioContextConfig.copy(stayAwake: value)),
),
],
);
}
Widget _androidTab() {
return TabContent(
children: [
Cbx(
'isSpeakerphoneOn',
value: audioContext.android.isSpeakerphoneOn,
({value}) => updateAudioContextAndroid(
audioContext.android.copy(isSpeakerphoneOn: value),
),
),
Cbx(
'stayAwake',
value: audioContext.android.stayAwake,
({value}) => updateAudioContextAndroid(
audioContext.android.copy(stayAwake: value),
),
),
LabeledDropDown<AndroidContentType>(
label: 'contentType',
key: const Key('contentType'),
options: {for (final e in AndroidContentType.values) e: e.name},
selected: audioContext.android.contentType,
onChange: (v) => updateAudioContextAndroid(
audioContext.android.copy(contentType: v),
),
),
LabeledDropDown<AndroidUsageType>(
label: 'usageType',
key: const Key('usageType'),
options: {for (final e in AndroidUsageType.values) e: e.name},
selected: audioContext.android.usageType,
onChange: (v) => updateAudioContextAndroid(
audioContext.android.copy(usageType: v),
),
),
LabeledDropDown<AndroidAudioFocus?>(
key: const Key('audioFocus'),
label: 'audioFocus',
options: {for (final e in AndroidAudioFocus.values) e: e.name},
selected: audioContext.android.audioFocus,
onChange: (v) => updateAudioContextAndroid(
audioContext.android.copy(audioFocus: v),
),
),
LabeledDropDown<AndroidAudioMode>(
key: const Key('audioMode'),
label: 'audioMode',
options: {for (final e in AndroidAudioMode.values) e: e.name},
selected: audioContext.android.audioMode,
onChange: (v) => updateAudioContextAndroid(
audioContext.android.copy(audioMode: v),
),
),
],
);
}
Widget _iosTab() {
final iosOptions = AVAudioSessionOptions.values.map(
(option) {
final options = {...audioContext.iOS.options};
return Cbx(
option.name,
value: options.contains(option),
({value}) {
updateAudioContextIOS(() {
final iosContext = audioContext.iOS.copy(options: options);
if (value ?? false) {
options.add(option);
} else {
options.remove(option);
}
return iosContext;
});
},
);
},
).toList();
return TabContent(
children: <Widget>[
LabeledDropDown<AVAudioSessionCategory>(
key: const Key('category'),
label: 'category',
options: {for (final e in AVAudioSessionCategory.values) e: e.name},
selected: audioContext.iOS.category,
onChange: (v) => updateAudioContextIOS(
() => audioContext.iOS.copy(category: v),
),
),
...iosOptions,
],
);
}
@override
bool get wantKeepAlive => true;
}

View File

@ -0,0 +1,243 @@
import 'package:audioplayers/audioplayers.dart';
import 'package:audioplayers_elinux_example/components/btn.dart';
import 'package:audioplayers_elinux_example/components/list_tile.dart';
import 'package:audioplayers_elinux_example/components/tab_content.dart';
import 'package:audioplayers_elinux_example/components/tgl.dart';
import 'package:audioplayers_elinux_example/components/txt.dart';
import 'package:audioplayers_elinux_example/utils.dart';
import 'package:flutter/material.dart';
class ControlsTab extends StatefulWidget {
final AudioPlayer player;
const ControlsTab({
required this.player,
super.key,
});
@override
State<ControlsTab> createState() => _ControlsTabState();
}
class _ControlsTabState extends State<ControlsTab>
with AutomaticKeepAliveClientMixin<ControlsTab> {
String modalInputSeek = '';
Future<void> _update(Future<void> Function() fn) async {
await fn();
// update everyone who listens to "player"
setState(() {});
}
Future<void> _seekPercent(double percent) async {
final duration = await widget.player.getDuration();
if (duration == null) {
toast(
'Failed to get duration for proportional seek.',
textKey: const Key('toast-proportional-seek-duration-null'),
);
return;
}
final position = duration * percent;
_seekDuration(position);
}
Future<void> _seekDuration(Duration position) async {
await _update(
() => widget.player.seek(position),
);
}
@override
Widget build(BuildContext context) {
super.build(context);
return TabContent(
children: [
WrappedListTile(
children: [
Btn(
key: const Key('control-pause'),
txt: 'Pause',
onPressed: widget.player.pause,
),
Btn(
key: const Key('control-stop'),
txt: 'Stop',
onPressed: widget.player.stop,
),
Btn(
key: const Key('control-resume'),
txt: 'Resume',
onPressed: widget.player.resume,
),
Btn(
key: const Key('control-release'),
txt: 'Release',
onPressed: widget.player.release,
),
],
),
WrappedListTile(
leading: const Text('Volume'),
children: [0.0, 0.5, 1.0, 2.0].map((it) {
final formattedVal = it.toStringAsFixed(1);
return Btn(
key: Key('control-volume-$formattedVal'),
txt: formattedVal,
onPressed: () => widget.player.setVolume(it),
);
}).toList(),
),
WrappedListTile(
leading: const Text('Balance'),
children: [-1.0, -0.5, 0.0, 1.0].map((it) {
final formattedVal = it.toStringAsFixed(1);
return Btn(
key: Key('control-balance-$formattedVal'),
txt: formattedVal,
onPressed: () => widget.player.setBalance(it),
);
}).toList(),
),
WrappedListTile(
leading: const Text('Rate'),
children: [0.0, 0.5, 1.0, 2.0].map((it) {
final formattedVal = it.toStringAsFixed(1);
return Btn(
key: Key('control-rate-$formattedVal'),
txt: formattedVal,
onPressed: () => widget.player.setPlaybackRate(it),
);
}).toList(),
),
WrappedListTile(
leading: const Text('Player Mode'),
children: [
EnumTgl<PlayerMode>(
key: const Key('control-player-mode'),
options: {
for (final e in PlayerMode.values)
'control-player-mode-${e.name}': e,
},
selected: widget.player.mode,
onChange: (playerMode) async {
await _update(() => widget.player.setPlayerMode(playerMode));
},
),
],
),
WrappedListTile(
leading: const Text('Release Mode'),
children: [
EnumTgl<ReleaseMode>(
key: const Key('control-release-mode'),
options: {
for (final e in ReleaseMode.values)
'control-release-mode-${e.name}': e,
},
selected: widget.player.releaseMode,
onChange: (releaseMode) async {
await _update(
() => widget.player.setReleaseMode(releaseMode),
);
},
),
],
),
WrappedListTile(
leading: const Text('Seek'),
children: [
...[0.0, 0.5, 1.0].map((it) {
final formattedVal = it.toStringAsFixed(1);
return Btn(
key: Key('control-seek-$formattedVal'),
txt: formattedVal,
onPressed: () => _seekPercent(it),
);
}),
Btn(
txt: 'Custom',
onPressed: () async {
dialog(
_SeekDialog(
value: modalInputSeek,
setValue: (it) => setState(() => modalInputSeek = it),
seekDuration: () => _seekDuration(
Duration(
milliseconds: int.parse(modalInputSeek),
),
),
seekPercent: () => _seekPercent(
double.parse(modalInputSeek),
),
),
);
},
),
],
),
],
);
}
@override
bool get wantKeepAlive => true;
}
class _SeekDialog extends StatelessWidget {
final VoidCallback seekDuration;
final VoidCallback seekPercent;
final void Function(String val) setValue;
final String value;
const _SeekDialog({
required this.seekDuration,
required this.seekPercent,
required this.value,
required this.setValue,
});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('Pick a duration and unit to seek'),
TxtBox(
value: value,
onChange: setValue,
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Btn(
txt: 'millis',
onPressed: () {
Navigator.of(context).pop();
seekDuration();
},
),
Btn(
txt: 'seconds',
onPressed: () {
Navigator.of(context).pop();
seekDuration();
},
),
Btn(
txt: '%',
onPressed: () {
Navigator.of(context).pop();
seekPercent();
},
),
TextButton(
onPressed: Navigator.of(context).pop,
child: const Text('Cancel'),
),
],
),
],
);
}
}

View File

@ -0,0 +1,185 @@
import 'package:audioplayers/audioplayers.dart';
import 'package:audioplayers_elinux_example/components/btn.dart';
import 'package:flutter/material.dart';
class LoggerTab extends StatefulWidget {
final AudioPlayer player;
const LoggerTab({
required this.player,
super.key,
});
@override
LoggerTabState createState() => LoggerTabState();
}
class LoggerTabState extends State<LoggerTab>
with AutomaticKeepAliveClientMixin<LoggerTab> {
AudioLogLevel get currentLogLevel => AudioLogger.logLevel;
set currentLogLevel(AudioLogLevel level) {
AudioLogger.logLevel = level;
}
List<Log> logs = [];
List<Log> globalLogs = [];
@override
void initState() {
super.initState();
AudioPlayer.global.onLog.listen(
(message) {
if (AudioLogLevel.info.level <= currentLogLevel.level) {
setState(() {
globalLogs.add(Log(message, level: AudioLogLevel.info));
});
}
},
onError: (Object o, [StackTrace? stackTrace]) {
if (AudioLogLevel.error.level <= currentLogLevel.level) {
setState(() {
globalLogs.add(
Log(
AudioLogger.errorToString(o, stackTrace),
level: AudioLogLevel.error,
),
);
});
}
},
);
widget.player.onLog.listen(
(message) {
if (AudioLogLevel.info.level <= currentLogLevel.level) {
final msg = '$message\nSource: ${widget.player.source}';
setState(() {
logs.add(Log(msg, level: AudioLogLevel.info));
});
}
},
onError: (Object o, [StackTrace? stackTrace]) {
if (AudioLogLevel.error.level <= currentLogLevel.level) {
setState(() {
logs.add(
Log(
AudioLogger.errorToString(
AudioPlayerException(widget.player, cause: o),
stackTrace,
),
level: AudioLogLevel.error,
),
);
});
}
},
);
}
@override
Widget build(BuildContext context) {
super.build(context);
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
ListTile(
title: Text(currentLogLevel.toString()),
subtitle: const Text('Log Level'),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: AudioLogLevel.values
.map(
(level) => Btn(
txt: level.toString().replaceAll('AudioLogLevel.', ''),
onPressed: () {
setState(() => currentLogLevel = level);
},
),
)
.toList(),
),
const Divider(color: Colors.black),
Expanded(
child: LogView(
title: 'Player Logs:',
logs: logs,
onDelete: () => setState(() {
logs.clear();
}),
),
),
const Divider(color: Colors.black),
Expanded(
child: LogView(
title: 'Global Logs:',
logs: globalLogs,
onDelete: () => setState(() {
globalLogs.clear();
}),
),
),
],
),
);
}
@override
bool get wantKeepAlive => true;
}
class LogView extends StatelessWidget {
final String title;
final List<Log> logs;
final VoidCallback onDelete;
const LogView({
required this.logs,
required this.title,
required this.onDelete,
super.key,
});
@override
Widget build(BuildContext context) {
return Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(title),
IconButton(onPressed: onDelete, icon: const Icon(Icons.delete)),
],
),
Expanded(
child: ListView(
children: logs
.map(
(log) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SelectableText(
'${log.level}: ${log.message}',
style: log.level == AudioLogLevel.error
? const TextStyle(color: Colors.red)
: null,
),
Divider(color: Colors.grey.shade400),
],
),
)
.toList(),
),
),
],
);
}
}
class Log {
Log(this.message, {required this.level});
final AudioLogLevel level;
final String message;
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,28 @@
import 'package:audioplayers/audioplayers.dart';
import 'package:audioplayers_elinux_example/components/player_widget.dart';
import 'package:audioplayers_elinux_example/components/properties_widget.dart';
import 'package:audioplayers_elinux_example/components/stream_widget.dart';
import 'package:audioplayers_elinux_example/components/tab_content.dart';
import 'package:flutter/material.dart';
class StreamsTab extends StatelessWidget {
final AudioPlayer player;
const StreamsTab({
required this.player,
super.key,
});
@override
Widget build(BuildContext context) {
return TabContent(
children: [
PlayerWidget(player: player),
const Divider(),
StreamWidget(player: player),
const Divider(),
PropertiesWidget(player: player),
],
);
}
}

View File

@ -0,0 +1,42 @@
import 'package:audioplayers/audioplayers.dart';
import 'package:audioplayers_elinux_example/components/dlg.dart';
import 'package:flutter/material.dart';
extension StateExt<T extends StatefulWidget> on State<T> {
void toast(String message, {Key? textKey}) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message, key: textKey),
duration: Duration(milliseconds: message.length * 25),
),
);
}
void simpleDialog(String message, [String action = 'Ok']) {
showDialog<void>(
context: context,
builder: (_) {
return SimpleDlg(message: message, action: action);
},
);
}
void dialog(Widget child) {
showDialog<void>(
context: context,
builder: (_) {
return Dlg(child: child);
},
);
}
}
extension PlayerStateIcon on PlayerState {
IconData getIcon() {
return this == PlayerState.playing
? Icons.play_arrow
: (this == PlayerState.paused
? Icons.pause
: (this == PlayerState.stopped ? Icons.stop : Icons.stop_circle));
}
}

View File

@ -0,0 +1,28 @@
name: audioplayers_elinux_example
description: Demonstrates how to use the audioplayers plugin.
publish_to: none
dependencies:
audioplayers: ^6.0.0
audioplayers_elinux:
path: ../
collection: ^1.16.0
file_picker: ^6.1.1
flutter:
sdk: flutter
http: ^1.0.0
path_provider_elinux:
path: ../../path_provider
provider: ^6.0.5
dev_dependencies:
flutter:
uses-material-design: true
assets:
- assets/
environment:
sdk: '>=3.0.0 <4.0.0'
flutter: '>=3.13.0'

View File

@ -0,0 +1,20 @@
name: audioplayers_elinux
description: Flutter plugin for playing audio with other Flutter widgets on Embedded Linux.
homepage: https://github.com/sony/flutter-elinux-plugins/
repository: https://github.com/sony/flutter-elinux-plugins/tree/main/packages/auidoplayers
version: 0.1.0
flutter:
plugin:
platforms:
elinux:
pluginClass: AudioplayersElinuxPlugin
dependencies:
audioplayers_platform_interface: ^7.0.0
flutter:
sdk: flutter
environment:
sdk: ">=2.18.0 <4.0.0"
flutter: ">=3.7.0"