mirror of
https://github.com/sony/flutter-elinux-plugins.git
synced 2025-08-06 15:11:38 +08:00
Add audioplayers for elinux (#96)
Signed-off-by: Makoto Sato <makoto.sato@atmark-techno.com>
This commit is contained in:
@ -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
7
packages/audioplayers/.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
.DS_Store
|
||||
.dart_tool/
|
||||
|
||||
.packages
|
||||
.pub/
|
||||
|
||||
build/
|
2
packages/audioplayers/CHANGELOG.md
Normal file
2
packages/audioplayers/CHANGELOG.md
Normal file
@ -0,0 +1,2 @@
|
||||
## 0.1.0
|
||||
* First draft version.
|
27
packages/audioplayers/LICENSE
Normal file
27
packages/audioplayers/LICENSE
Normal 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.
|
35
packages/audioplayers/README.md
Normal file
35
packages/audioplayers/README.md
Normal 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';
|
||||
```
|
41
packages/audioplayers/elinux/CMakeLists.txt
Normal file
41
packages/audioplayers/elinux/CMakeLists.txt
Normal 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
|
||||
)
|
55
packages/audioplayers/elinux/audio_player_stream_handler.h
Normal file
55
packages/audioplayers/elinux/audio_player_stream_handler.h
Normal 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_
|
@ -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_
|
307
packages/audioplayers/elinux/audioplayers_elinux_plugin.cc
Normal file
307
packages/audioplayers/elinux/audioplayers_elinux_plugin.cc
Normal 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));
|
||||
}
|
370
packages/audioplayers/elinux/gst_audio_player.cc
Normal file
370
packages/audioplayers/elinux/gst_audio_player.cc
Normal 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;
|
||||
}
|
73
packages/audioplayers/elinux/gst_audio_player.h
Normal file
73
packages/audioplayers/elinux/gst_audio_player.h
Normal 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_
|
@ -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_
|
8
packages/audioplayers/example/README.md
Normal file
8
packages/audioplayers/example/README.md
Normal 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).
|
BIN
packages/audioplayers/example/assets/ambient_c_motion.mp3
Normal file
BIN
packages/audioplayers/example/assets/ambient_c_motion.mp3
Normal file
Binary file not shown.
BIN
packages/audioplayers/example/assets/coins whitespace.wav
Normal file
BIN
packages/audioplayers/example/assets/coins whitespace.wav
Normal file
Binary file not shown.
BIN
packages/audioplayers/example/assets/coins.mp3
Normal file
BIN
packages/audioplayers/example/assets/coins.mp3
Normal file
Binary file not shown.
BIN
packages/audioplayers/example/assets/coins.wav
Normal file
BIN
packages/audioplayers/example/assets/coins.wav
Normal file
Binary file not shown.
BIN
packages/audioplayers/example/assets/coins_no_extension
Normal file
BIN
packages/audioplayers/example/assets/coins_no_extension
Normal file
Binary file not shown.
BIN
packages/audioplayers/example/assets/coins_non_ascii_и.wav
Normal file
BIN
packages/audioplayers/example/assets/coins_non_ascii_и.wav
Normal file
Binary file not shown.
1
packages/audioplayers/example/assets/invalid.txt
Normal file
1
packages/audioplayers/example/assets/invalid.txt
Normal file
@ -0,0 +1 @@
|
||||
This represents an invalid audio file.
|
BIN
packages/audioplayers/example/assets/laser.wav
Normal file
BIN
packages/audioplayers/example/assets/laser.wav
Normal file
Binary file not shown.
BIN
packages/audioplayers/example/assets/nasa_on_a_mission.mp3
Normal file
BIN
packages/audioplayers/example/assets/nasa_on_a_mission.mp3
Normal file
Binary file not shown.
103
packages/audioplayers/example/elinux/CMakeLists.txt
Normal file
103
packages/audioplayers/example/elinux/CMakeLists.txt
Normal 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()
|
108
packages/audioplayers/example/elinux/flutter/CMakeLists.txt
Normal file
108
packages/audioplayers/example/elinux/flutter/CMakeLists.txt
Normal 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}
|
||||
)
|
@ -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)
|
23
packages/audioplayers/example/elinux/runner/CMakeLists.txt
Normal file
23
packages/audioplayers/example/elinux/runner/CMakeLists.txt
Normal 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)
|
402
packages/audioplayers/example/elinux/runner/command_options.h
Normal file
402
packages/audioplayers/example/elinux/runner/command_options.h
Normal 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_
|
@ -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_
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
34
packages/audioplayers/example/elinux/runner/flutter_window.h
Normal file
34
packages/audioplayers/example/elinux/runner/flutter_window.h
Normal 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_
|
53
packages/audioplayers/example/elinux/runner/main.cc
Normal file
53
packages/audioplayers/example/elinux/runner/main.cc
Normal 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;
|
||||
}
|
24
packages/audioplayers/example/lib/components/btn.dart
Normal file
24
packages/audioplayers/example/lib/components/btn.dart
Normal 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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
23
packages/audioplayers/example/lib/components/cbx.dart
Normal file
23
packages/audioplayers/example/lib/components/cbx.dart
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
48
packages/audioplayers/example/lib/components/dlg.dart
Normal file
48
packages/audioplayers/example/lib/components/dlg.dart
Normal 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
60
packages/audioplayers/example/lib/components/drop_down.dart
Normal file
60
packages/audioplayers/example/lib/components/drop_down.dart
Normal 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(),
|
||||
);
|
||||
}
|
||||
}
|
26
packages/audioplayers/example/lib/components/list_tile.dart
Normal file
26
packages/audioplayers/example/lib/components/list_tile.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
16
packages/audioplayers/example/lib/components/pad.dart
Normal file
16
packages/audioplayers/example/lib/components/pad.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
178
packages/audioplayers/example/lib/components/player_widget.dart
Normal file
178
packages/audioplayers/example/lib/components/player_widget.dart
Normal 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;
|
||||
});
|
||||
}
|
||||
}
|
@ -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),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -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),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
52
packages/audioplayers/example/lib/components/tabs.dart
Normal file
52
packages/audioplayers/example/lib/components/tabs.dart
Normal 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,
|
||||
});
|
||||
}
|
61
packages/audioplayers/example/lib/components/tgl.dart
Normal file
61
packages/audioplayers/example/lib/components/tgl.dart
Normal 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]),
|
||||
);
|
||||
}
|
||||
}
|
38
packages/audioplayers/example/lib/components/txt.dart
Normal file
38
packages/audioplayers/example/lib/components/txt.dart
Normal 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);
|
||||
}
|
||||
}
|
208
packages/audioplayers/example/lib/main.dart
Normal file
208
packages/audioplayers/example/lib/main.dart
Normal 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,
|
||||
}
|
246
packages/audioplayers/example/lib/tabs/audio_context.dart
Normal file
246
packages/audioplayers/example/lib/tabs/audio_context.dart
Normal 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;
|
||||
}
|
243
packages/audioplayers/example/lib/tabs/controls.dart
Normal file
243
packages/audioplayers/example/lib/tabs/controls.dart
Normal 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'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
185
packages/audioplayers/example/lib/tabs/logger.dart
Normal file
185
packages/audioplayers/example/lib/tabs/logger.dart
Normal 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;
|
||||
}
|
471
packages/audioplayers/example/lib/tabs/sources.dart
Normal file
471
packages/audioplayers/example/lib/tabs/sources.dart
Normal file
File diff suppressed because one or more lines are too long
28
packages/audioplayers/example/lib/tabs/streams.dart
Normal file
28
packages/audioplayers/example/lib/tabs/streams.dart
Normal 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),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
42
packages/audioplayers/example/lib/utils.dart
Normal file
42
packages/audioplayers/example/lib/utils.dart
Normal 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));
|
||||
}
|
||||
}
|
28
packages/audioplayers/example/pubspec.yaml
Normal file
28
packages/audioplayers/example/pubspec.yaml
Normal 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'
|
20
packages/audioplayers/pubspec.yaml
Normal file
20
packages/audioplayers/pubspec.yaml
Normal 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"
|
Reference in New Issue
Block a user