I'm trying to play an audio file on iOS with custom written platform channel. I have no errors when trying to play it, but I can't hear any sound from the phone/simulator.
When I try just_audio everything works great, so the problem is not about file path. You can test the commented method of playing it with just_audio. But when I try to play it through the platform channel, I can't hear any sound
This is my AppDelegate.swift:
import UIKit
import Flutter
import AVFoundation
#UIApplicationMain
#objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
let playerChannel = FlutterMethodChannel(name: "PlayerChannel",
binaryMessenger: controller.binaryMessenger)
playerChannel.setMethodCallHandler({
[weak self] (call: FlutterMethodCall, result: FlutterResult) -> Void in
// This method is invoked on the UI thread.
guard call.method == "play" else {
result(FlutterMethodNotImplemented)
return
}
guard let args = call.arguments as? [String : Any] else {return}
let text = args["path"] as! String
self?.playAudio(result: result, path: text)
})
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
private func playAudio(result: FlutterResult, path: String) {
do{
let audioData = try Data.init(contentsOf:URL(fileURLWithPath: path))
try AVAudioSession.sharedInstance().setMode(.default)
try AVAudioSession.sharedInstance().setActive(true)
let audioPlayer = try AVAudioPlayer.init(data: audioData)
audioPlayer.play()
} catch {
print(String(describing: error))
result(FlutterError(code: "UNAVAILABLE",
message: "error.",
details: nil))
}
}
}
And this is how I call this method from my dart file:
import 'dart:developer';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:just_audio/just_audio.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const HomePage(),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
#override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
static const platform = MethodChannel('PlayerChannel');
Future<String> getPath() async {
Directory tempDir = await getTemporaryDirectory();
String tempPath = tempDir.path;
var filePath = "$tempPath/brown_noise.mp3";
var file = File(filePath);
if (file.existsSync()) {
return file.path;
} else {
final byteData = await rootBundle.load('assets/brown_noise.mp3');
final buffer = byteData.buffer;
await file.create(recursive: true);
file.writeAsBytes(
buffer.asUint8List(byteData.offsetInBytes, byteData.lengthInBytes));
return file.path;
}
}
#override
void initState() {
super.initState();
Future.delayed(const Duration(milliseconds: 800), () async {
try {
await platform.invokeMethod('play', {'path': await getPath()});
} on PlatformException catch (e) {
log("Failed to play: '${e.message}'.");
}
/* final a = AudioPlayer();
a.setFilePath(await getPath());
a.play(); */
});
}
#override
Widget build(BuildContext context) {
return const Placeholder();
}
}
Related
I have a function that downloads mp3 file from URL, passes it to AVAudioPlayer and then plays it in PlayerView. I want to implement a feature. When a mp3 will be downloaded, I want to be cached in the app files so If I open it later It wouldn't be downloaded. I saw tutorials of how to do this with Images, but not with mp3. How can this be created?
// Audio Manager itself
import Foundation
import AVFoundation
import AVFAudio
final class AudioManager: ObservableObject {
// static let shared = AudioManager()
var player: AVAudioPlayer?
#Published private(set) var isPlaying: Bool = false {
didSet {
print(isPlaying, "isPlaying")
}
}
func startPlayer(track: String) {
guard let fileURL = URL(string: track) else { return }
do {
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default)
try AVAudioSession.sharedInstance().setActive(true)
let soundData = try Data(contentsOf: fileURL)
self.player = try AVAudioPlayer(data: soundData)
guard let player = player else { return }
player.prepareToPlay()
player.play()
isPlaying = true
}
catch {
print(error)
}
}
func playPause() {
guard let player = player else {
print("Audio player not found")
return
}
if player.isPlaying {
player.pause()
isPlaying = false
} else {
player.play()
isPlaying = true
}
}
func stop() {
guard let player = player else {
print("Audio player not found")
return
}
if player.isPlaying {
player.stop()
isPlaying = false
}
}
}
// Main thing in my PlayerView. Passes the track to the audioManager
.onAppear {
// AudioManager.shared.startPlayer(track: "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3")
DispatchQueue.main.async {
audioManager.startPlayer(track: track ?? "")
}
}
A simple way to do this would just be to write the Data that you download straight to a file. The next time you try to play that track, check if a file for it exists and load that local file instead.
Here's a (fairly naive) example:
final class AudioManager: ObservableObject {
// static let shared = AudioManager()
var player: AVAudioPlayer?
#Published private(set) var isDownloading = false
#Published private(set) var isPlaying: Bool = false
// MainActor so it always runs on the main queue
#MainActor func startPlayer(track: String) async {
guard let url = URL(string: track) else { return }
do {
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default)
try AVAudioSession.sharedInstance().setActive(true)
let songName = url.lastPathComponent
var soundData: Data
let tracksFolderUrl = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).last!.appendingPathComponent("tracks")
let trackUrl = tracksFolderUrl.appendingPathComponent(songName)
if FileManager.default.fileExists(atPath: trackUrl.path) {
// Load local data if it exists
print("Loading data from \(trackUrl)")
soundData = try Data(contentsOf: trackUrl)
} else {
//… otherwise load from network
isDownloading = true
print("Downloading data from \(url)")
(soundData, _) = try await URLSession.shared.data(from: url)
//… then save to disk
try FileManager.default.createDirectory(at: tracksFolderUrl, withIntermediateDirectories: true)
print("Saving data to \(trackUrl)")
try soundData.write(to: trackUrl)
isDownloading = false
}
self.player = try AVAudioPlayer(data: soundData)
guard let player = player else { return }
player.prepareToPlay()
player.play()
isPlaying = true
}
catch {
print(error)
}
}
}
struct ContentView: View {
#StateObject var audioManager = AudioManager()
var body: some View {
ZStack {
if audioManager.isDownloading {
VStack {
Text("Downloading")
ProgressView()
}
} else {
Text("Playing")
}
}
.task {
await audioManager.startPlayer(track: "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3")
}
}
}
Note that I've made the startPlayer func async so it doesn't block the main thread and used a different method to download the data
try await URLSession.shared.data(from: url)
I am using flutter_sound to record voice(audio).
I can't see any file save on the emulator after the recording stops.
Recorder class:
import 'package:flutter/material.dart';
import 'package:flutter_sound/flutter_sound.dart';
import 'package:permission_handler/permission_handler.dart';
final _pathToAudio = 'myFile.aac';
class Recorder {
FlutterSoundRecorder? _recorder;
bool _isRecorderInitialized = false;
bool get isRecording => _recorder!.isRecording;
Future init() async {
_recorder = FlutterSoundRecorder();
final status = await Permission.microphone.request();
if (status != PermissionStatus.granted) {
throw RecordingPermissionException('Recording permission required.');
}
await _recorder!.openAudioSession();
_isRecorderInitialized = true;
}
void dispose() {
_recorder!.closeAudioSession();
_recorder = null;
_isRecorderInitialized = false;
}
Future record() async {
if (!_isRecorderInitialized) {
return;
}
print('recording....');
await _recorder!.startRecorder(
toFile: _pathToAudio,
codec: Codec.aacADTS,
);
}
Future stop() async {
if (!_isRecorderInitialized) {
return;
}
await _recorder!.stopRecorder();
print('stopped....'); //I GET THIS MESSAGE SO I AM GUESSING THE RECORDING IS HAPPENING BUT CANT FIND THE FILE LATER IN THE EMULATOR.
}
Future toggleRecording() async {
if (_recorder!.isStopped) {
await record();
} else {
await stop();
}
}
}
Function on button click:
void startStopRecording() async {
final isRecordingOn = await recorder.toggleRecording();
setState(() {
_isRecording = recorder.isRecording;
});
if(_isRecording) {
startTimer();
_stopwatch.start();
} else {
stopTimer();
_stopwatch.stop();
}
}
I'm just going to paste in a couple of my files so that you can test this really easily and see what's going on. I'm clicking the button and it's making the shortened dynamic link. Then, I'm typing out the DynamicLink in the notes app and then I press the link. I get redirected to the app and the following error is returned:
[connection] nw_read_request_report [C1] Receive failed with error "Software caused connection abort"
Side note: all of this is being tested on an iPhone 7 (a physical device, not the simulator).
FirebaseTestApp and AppDelegate:
import SwiftUI
import Firebase
#main
struct FirebaseTestApp: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
class AppDelegate: NSObject, UIApplicationDelegate {
var functionMaster: functions = functions()
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
FirebaseApp.configure()
return true
}
func application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) -> Bool {
let dynamicLink = DynamicLinks.dynamicLinks().dynamicLink(fromCustomSchemeURL: url)
if dynamicLink != nil {
print("Dynamic link : \(String(describing: dynamicLink?.url))")
return true
}
return false
}
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: #escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
print("Successful penetration")
guard let inComingURL = userActivity.webpageURL else { return false }
print("Incoming Web Page URL: \(inComingURL)")
self.functionMaster.handleIncomingDynamicLink(inComingURL)
return true
}
}
functions class:
import Foundation
import Firebase
import UIKit
class functions: ObservableObject {
func makeDynamicLink() {
var components = URLComponents()
components.scheme = "https"
components.host = "www.firebase-test.com" //this can be some random domain right? It doesn't have to actually exist yet?
components.path = "/data"
let stringifiedNumber = String(123)
components.queryItems = [stringifiedNumber]
let dynamicLinksDomainURIPrefix = "https://something.page.link"
guard let linkParameter = components.url else { return }
print("I am sharing \(linkParameter)")
guard let linkBuilder = DynamicLinkComponents(link: linkParameter, domainURIPrefix: dynamicLinksDomainURIPrefix) else { return }
if let myBundleId = Bundle.main.bundleIdentifier {
linkBuilder.iOSParameters = DynamicLinkIOSParameters(bundleID: myBundleId)
}
linkBuilder.iOSParameters?.appStoreID = "962194608"
linkBuilder.socialMetaTagParameters = DynamicLinkSocialMetaTagParameters()
linkBuilder.socialMetaTagParameters?.title = testLocation.name
linkBuilder.socialMetaTagParameters?.descriptionText = testLocation.address
linkBuilder.shorten { [weak self] (url, warnings, error) in
if let error = error{
print("Firebase encountered an error: \(error)")
return
}
if let warnings = warnings {
for warning in warnings {
print("Firebase Warning: \(warning)")
}
}
guard let url = url else { return }
print("The short URL is: \(url.absoluteString)")
self?.showShareSheet(url: url)
}
guard let longDynamicLink = linkBuilder.url else { return }
print("The long URL is: \(longDynamicLink)")
}
func showShareSheet(url: URL) {
let promoText = "Check out this thing I've marked in FirebaseTest!"
let activityVC = UIActivityViewController(activityItems: [promoText, url], applicationActivities: nil)
UIApplication.shared.windows.first?.rootViewController?.present(activityVC, animated: true)
}
func handleIncomingDynamicLink(_ dynamicLink: URL) {
_ = DynamicLinks.dynamicLinks().handleUniversalLink(dynamicLink) { (dynamiclink, error) in
guard error == nil else {
print("Found an error: \(error?.localizedDescription ?? "")")
return
}
print("Dynamic link : \(String(describing: dynamiclink?.url))")
let path = dynamiclink?.url?.path
var id = 0
if let query = dynamiclink?.url?.query {
let dataArray = query.components(separatedBy: "=")
id = Int(dataArray[1]) ?? 0
}
if path == "data" {
//Write code here
}
}
}
}
ContentView:
import SwiftUI
struct ContentView: View {
#ObservedObject var functionMaster: functions = functions()
var body: some View {
Button("Click me to run some firebase stuff") {
functionMaster.makeDynamicLink()
}
.padding()
}
}
In browser, when I navigate to https://something.page.link/apple-app-site-association, I get this:
https://i.stack.imgur.com/6Ndo0.png
Try installing the files for the the simulator you want to test on, update Xcode, delete all other versions.
I am using WorkManager for Background Service. My code is as follows
void callbackDispatcher() {
Workmanager.executeTask((task, inputData) async {
switch (task) {
case "uploadLocalData":
print("this method was called from background!");
await BackgroundProcessHandler().uploadLocalData();
print("background Executed!");
return true;
break;
case Workmanager.iOSBackgroundTask:
print("iOS background fetch delegate ran");
return true;
break;
}
return false;
});
}
is there any way to wait for async method in executeTask ?
The way to asyncify non-async things is through completers. You create a completer, await on its future and complete it in the future.
Completer uploadCompleter = Completer();
void callbackDispatcher() {
Workmanager.executeTask((task, inputData) async {
switch (task) {
case "uploadLocalData":
print("this method was called from background!");
await BackgroundProcessHandler().uploadLocalData();
print("background Executed!");
uploadCompleter.complete();
return true;
break;
case Workmanager.iOSBackgroundTask:
print("iOS background fetch delegate ran");
return true;
break;
}
return false;
});
}
// somewhere else
await uploadCompleter.future;
I changed the permissions on the Firebase console and set to allow all users access without the need for an authentication.
I have the following code:
AppDelegate.swift
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
{
FirebaseApp.configure()
Utils.initApp()
return true
}
Utils.swift
import Foundation
import Firebase
class Utils
{
static var inventoryJsonString = "Inventory/Inventory.json"
static var oneMB : Int64 = 1024 * 1024
static func initApp()
{
getJsonDate(inventoryJsonString)
}
static func getJsonData(filePath: String)
{
let storageRef = Storage.storage().reference()
let jsonRef = storageRef.child("Inventory")
jsonRef.getData(maxSize: self.oneMB)
{
extractedData, error in
print("a")
if let error = error{
print("b")
}
else
{
print("c")
}
}
}
I'm calling that function but nothing happnes - I don't get an error, yet I'm not getting the url (also tried with getData and got nothing). I tripple checked the path in filePath and it's correct.
What am I missing here?
I assume you're trying to read the actual json file, not all the files within the Inventory path
Here's your code with notes on how to fix:
class Utils
{
static var inventoryJsonString = "Inventory/Inventory.json" //NOT USED!
static var oneMB : Int64 = 1024 * 1024
static func initApp() {
getJsonDate(inventoryJsonString)
}
static func getJsonData(filePath: String) { //filePath is NOT USED!
let storageRef = Storage.storage().reference()
**this is a PATH to the enclosing directory only and not the JSON file**
let enclosingPathRef = storageRef.child("Inventory")
**add this to make it work**
let actualFileRef = enclosingPathRef.child("Inventory.json")
actualFileRef.getData(maxSize: self.oneMB) { extractedData, error in
if let error = error{
print("an error occurred: \(error.localizedDescription")
} else {
print("success!")
}
}
}
}