FC2ブログ

スマフォのアプリを作りたい(22):音声ファイル(.m4a)を再生する

   プログラミング [2020/04/16]
前々回までに音声認識と同時にその音声を録音する機能を作りました。
録音した音声は、.m4aファイルとして、保存してあります。
その音声ファイルを再生する部分をやってみます。

いつものボヤキに入る前に。

「スマフォのアプリを作りたい」シリーズを最初から読まないとどういう環境でやってることなのか分からないかと思いますので、ここでさらっと説明しておきます。
React Nativeを開発環境としています。
 開発環境のmustな前提/構成については省略します。
 スマフォのアプリを作りたい(2)~(4)辺りには書いてあります。
-最初のターゲットとしてはAndroidスマフォとうことにしています。
-動作環境は、AndroidStudioのAVD(エミュレータ)を現状は使用しています。
 (未だ実機での動作を一切確認したことがありません。)
Build、Load&Goは、「react-native run-android」コマンドで行っています。
-デバッガは、Chromeのデベロッパーツールです。
-コードの編集は、sakuraを使っちゃってます。つまり、AndroidStudioは使ってません。
 (使い方を現時点で知りません。)
-パッケージ(Javaコード)に手を入れてしまった場合は、デバッガは使ってなくて、
 logcatのメッセージログを使ってます。
-これらの開発・動作環境を全てタブレットPC(+SSD増設HDD)に突っ込んでます。
-OSはWindows10です。
-外部との接続は、主にポケットWifiです。

かなり、素朴なやり方ですかね。
そもそも、タブレットPCなので、そんなにパワーはなくて、サクサクというわけには行きません。
「react-native run-android」がかなりか確率で失敗するので、効率は良くありません。
また、Chromeでのデバッグもとっても重い印象です。
でも、「持って歩ける開発環境」です。

では、本題に入りましょう。


◆音声ファイルの再生には何を使う?
ちょっと前に、「録音」に関しては「react-native-audio」を使いかけました。
これで、「再生」もできるのかと思いきや・・・録音だけの機能なんだそうです。
再生」は、「react-native-sound」を使うのが、どうも定番っぽい?

でも、.m4aは再生できるんでしょうか?
ちょっとぐぐってみました。
参考:http://www.npmjs.com/package/react-native-sound
本家?Ver.1.11.0では、アルファ版だからバグがあるかもしれないよと書いてます。
また、ストリーミング再生はサポートしてないとある。
.m4aが扱えるのかは、上記URLのページ内には書かれていない。
んーー。

一方、以下のサイトには「.m4a」の単語が含まれていました。
参考:https://reactnativeexample.com/play-or-stream-audio-files-in-reactnative-on-ios-android/
タイトルからしてファイル再生とストリーミングもできるっぽい。
パッケージ名はちょっと長くなって」「react-native-sound-player」だそうです。
.m4aがサポートされているかどうかは、Android本家のサイトにリンクがされてあって、
参考:https://developer.android.com/guide/topics/media/media-formats
サポートされてるっぽいぞ。

ちなみに「AVPlayer(iOS)」と書かれている方(リンク先がなぜかstackoverflowのページ)では・・・
iOSのバージョン次第ってことなんでしょうか。10.9.1で問い合わせたときのリストには「public.mpeg-4-audio」がある。
まあ、そっちはまた後で考えるということで。

さるがこの手のページに慣れて来たせいもあって、説明も割と分かりやすいという第一印象。
なので、react-native-sound-playerを使ってみます。


◆追加インストール
参考:https://reactnativeexample.com/play-or-stream-audio-files-in-reactnative-on-ios-android/
に従ってインストールしてみます。

いつもの通りプロジェクトフォルダにカレントを移動して「npm install ・・・」と
いつもと違う「react-native link ・・・」を実行します。(でも、実は実行しなくてよかった。
----コマンドプロンプト-----------------------------
:
D:\AppMake\proj\VTChat>npm install react-native-sound-player --save
npm WARN @typescript-eslint/eslint-plugin@1.13.0 requires a peer of eslint@^5.0.0 but none is installed. You must install peer dependencies yourself.
npm WARN @typescript-eslint/parser@1.13.0 requires a peer of eslint@^5.0.0 but none is installed. You must install peer dependencies yourself.


+ react-native-sound-player@0.10.4
added 1 package from 1 contributor and audited 957490 packages in 64.888s


D:\AppMake\proj\VTChat>react-native link react-native-sound-player
info Linking "react-native-sound-player" iOS dependency
info iOS module "react-native-sound-player" has been successfully linked
info Linking "react-native-sound-player" Android dependency
info Android module "react-native-sound-player" has been successfully linked
---------------------------------------------------

何事もなく成功したようです。

ですが、「react-native run-android」したときに以下のエラーが出ました。
----コマンドプロンプト-----------------------------
D:\AppMake\proj\VTChat>react-native run-android
error React Native CLI uses autolinking for native dependencies, but the following modules are linked manually:
- react-native-fs (to unlink run: "react-native unlink react-native-fs")
- react-native-sound-player (to unlink run: "react-native unlink react-native-sound-player")
This is likely happening when upgrading React Native from below 0.60 to 0.60 or above. Going forward, you can unlink this dependency via "react-native unlink " and it will be included in your app automatically. If a library isn't compatible with autolinking, disregard this message and notify the library maintainers.
---------------------------------------------------

もし、さると同じように、サイトの説明を斜めに読んで「react-native link ・・・」を実行してしまった場合は、「react-native unlink ・・・」を実行するとエラーは消えます。

さるにとって「link」というパラメタ指定は鬼門のようです。

あと、本題には関係ないですが、録音音声のファイルリストを表示するために「react-native-fs」をインストールします。
手順としては、「npm install react-native-fs --save」を実行するだけです。
※本家のサイト等に「react-native link react-native-fs」を実行するように書いてありますが、さるの環境では必要ないらしい。また、本家サイトのUsageには、settings.gradle等に何か修正を加えるような記述が書かれているけど、DocumentDirectory以外をアクセスする場合に必要な設定なのかな? 幸いにして今回扱う録音ファイルはDocumentDirectory下なので必要ないはず。


それでは、実装に入ります・・・


◆音声ファイルの再生を実装
やりたいことの凡そのイメージは、
・前回の記事でも修正したApp.jsに再生メニューを追加します。
・再生メニュー(コンポーネント)は、音声ファイル名リストを表示します。
・表示されたファイル名をタップすると、ポップアップを表示してそのファイルの再生を開始します。
です。

まずは、App.jsへの追加です。
※修正前の全文は前回記事の後半をご参照ください。
----JSX:App.js-------------------------------------

import Speech from './Speech';
import PlayList from './PlayList';
:
:
<Scene key='Text'
component={Speech}
title="Speech:音声認識&録音"
iconName='mic'
iconColor='red'
icon={TabBarIcon}
initial
/>
<Scene key='Play'
component={PlayList}
title="Play:音声再生"
iconName='speaker'
iconColor='red'
icon={TabBarIcon}
/>

<Scene key='Chat'
component={Chat}
:
:
---------------------------------------------------


次に、新規で作成したPlayList.jsです。
こんな風にしました。
----JSX:PlayList.js-------------------------------------
/**
* Sample React Native App
* https://github.com/facebook/react-native
*
* @format
* @flow
*/


import {
//Platform,
SafeAreaView,
ScrollView,
Text,
View,
Button,
Alert,
//StyleSheet,
} from 'react-native';

import React, { Component } from 'react';
import SoundPlayer from 'react-native-sound-player'
import * as Fs from 'react-native-fs';

export default class PlayList extends Component {
constructor(props) {
super(props);
this.state = {
currentFile: "",
play_state: 0,
diritems : [],
};
//console.log('PlayList: diritems count', this.state.diritems.length);
this.dirpath = Fs.DocumentDirectoryPath + "/";
this.onFinishedPlayingSub = null;
//this.onFinishedLoadingSub = null;
//this.onFinishedLoadingFileSub = null;
//this.onFinishedLoadingURLSub = null;

}

//Mount処理
componentDidMount() {
//componentDidMount = async () => {
//# ファイルリスト取得
//# a)一般的サンプル記述:

Fs.readDir(this.dirpath)
//正常時
.then((result) => {
console.log('PlayList: readDir>result:', result);
//# このクラスのステートを更新→render()
this.setState({
diritems: result,
});
})
//?
.then((statResult) => {
console.log('PlayList: readDir>startResult:', startResult);
if (statResult[0].isFile()) {
console.log('PlayList: readDir>startResult: empty');
//return Fs.readFile(statResult[1], 'utf8');
}
//return 'no file';
})
//?
.then((contents) => {
console.log('PlayList: readDir>contents:', contents);
//return
})
.catch((err) => {
console.log('PlayList: '+err.message, err.code);
});
//# b)代替的記述:awaitするためにcomponentDidMount()にasyncを指定する必要あり
/*
var files = new Array();
files = await Fs.readDir(this.dirpath);
this.setState({
diritems: files,
});
*/


//# 再生終了時のコールバック登録処理
//# SoundPlayerのマウント

this.onFinishedPlayingSub = SoundPlayer.addEventListener('FinishedPlaying', ({success}) => {
console.log('PlayList: end playing', success);
this.setState({
currentFile: "",
play_state: 0,
});
});
//# 未使用のためコメントアウト/将来利用
//this.onFinishedLoadingSub = SoundPlayer.addEventListener('FinishedLoading', ({success}) => {
// console.log('PlayList: finished playing', success);
//});
//this.onFinishedLoadingFileSub = SoundPlayer.addEventListener('FinishedLoadingFile', ({success}) => {
// console.log('PlayList: finished playing', success);
//});
//this.onFinishedLoadingURLSub = SoundPlayer.addEventListener('FinishedLoadingURL', ({success}) => {
// console.log('PlayList: finished playing', success);
//});

}


//# Unmount処理
componentWillUnmount() {
//# SoundPlayerのアンマウント
if (this.onFinishedPlayingSub != null) {
this.onFinishedPlayingSub.remove();
}
//this.onFinishedLoadingSub.remove();
//this.onFinishedLoadingFileSub.remove();
//this.onFinishedLoadingURLSub.remove();

}


startPlaying = (item) => {
try {
if (this.state.play_state == 0) {
if (this.onFinishedPlayingSub == null) {
console.log('PlayList: failed to addEventListener');
Alert.alert("Failed to setup the SoundPlayer");
return;
}
//let filepath_noext = item.path.slice(0,(item.path.lastIndexOf('.')));
//SoundPlayer.playSoundFile(filepath_noext, "m4a");

let audioUri = "file://" + item.path;
//# playUrl()の処理結果を受け取るためのコールバック処理指定
//# 使い捨ての登録なので、SoundPlayer.playUrl実施の度に必要

SoundPlayer.setCallbackForOpen((fsuccess) => {
console.log('PlayList: sound open-'+fsuccess);
if (!fsuccess) {
this.setState({
currentFile: "",
play_state: 0,
});
Alert.alert("Failed to start the SoundPlayer");
}
});
SoundPlayer.playUrl(audioUri);
console.log('PlayList: sound open-success?');
this.setState({
currentFile: item.name,
play_state: 1,
});
}
}
catch (e) {
console.log('PlayList: cannot play the '+item.name, e);
}
}


putbutton = () => {
if (this.state.diritems.length > 0) {
return (this.state.diritems.map((item) => {
let fileext = item.name.slice((item.name.lastIndexOf('.')-1>>>0)+2); //ファイル拡張子の取出し
if (fileext.toUpperCase() === "M4A") {
if (item.name == this.state.currentFile) {
return (
<Button title={item.name}
//onPress={}
//style={styles.buttonDisable}

color='#ff0000'
/>
);
}
else {
return (
<Button title={item.name}
onPress={() => this.startPlaying(item)}
//style={styles.buttonEnable}
color='#0000ff'
/>
);
}
}
}));
}
};


render() {
return (
<SafeAreaView>
<ScrollView contentInsetAdjustmentBehavior="automatic">
<View style={{ margin: 20 }}>
{this.putbutton()}
</View>
</ScrollView>
</SafeAreaView>
);
}

}

/*
const styles = StyleSheet.create({
buttonEnable: {
margin: 5,
fontSize: 24,
fontWeight: 'bold',
textAlign: 'center',
color: '#004488',
backgroundColor: '#0000ff',
width: 24,
height: 24,
},
buttonDisable: {
margin: 5,
fontSize: 24,
fontWeight: 'bold',
textAlign: 'center',
color: '#004488',
backgroundColor: '#ff0000',
width: 24,
height: 24,
},
});
*/

---------------------------------------------------


ここで、いくつかのまつがい右往左往したことを書きます。


1)Primiseの扱いがよくわからなくて
ファイルリストを取得する処理はcomponentDidMountメソッドの中で行いたいと思って、react-native-fsのreadDirメソッドを使おうとしました。
readDirは、ファイル情報の配列を取得できるメソッドなんですが、Promise型(あえて「型」と呼びます)なんですね。
つまり非同期で実行されるってことです。

さるは別に非同期でファイルリストを取得したいわけではなく、readDir()の終わりを待って順次続きの処理がしたいだけ。

そうなると、以下のようにawaitを指定して終わりを待つという手もある。
files = await Fs.readDir(this.dirpath);

ですがこの場合、awaitでメソッドを呼出す側をasyncに指定する必要があるそうだ。
そうしないとawaitは使えない決・ま・り。(なんで、こんな余計な決まりが?)


一旦、そういう実装にしてみました。
componentDidMountメソッドasync(非同期)に実行するという指定にしました。
エラーにはなりませんでした。
でもなんか、危険な臭いがするのでそれは止めました。
readDirはPromiseらしく使って、結果で改めてsetState()して表示を更新することにしました。

そうなると、ファイルリストdiritemsが空配列のまま最初のrender()が実行されることになります。
その際のButtonコンポーネント作成処理(putbutton)内にスキップする処理を追加しました。
if (this.state.diritems.length > 0) {・・・}

readDirがリターンするファイル情報の構成は、以下本家サイトに記載があります。
参考:https://github.com/itinance/react-native-fs


2)ButtonコンポーネントのonPress時メソッドの指定?
ボタンを押した際に、そのボタンの受け持ちのファイル情報を渡したくて、最初「<Button ・・・onPress={this.startPlaying(item)} ・・・/>」とか指定していました。これは、NGだそうです。

使用するメソッド名に()をつけると、「<Button ・・・/>」を作成時点で「this.startPlaying(item)」を実行してそのリターンを「onPress=」に指定するという意味になるんだそうです。
そりゃそうかなとも思いますが、何だかややこしい。


色々対処の仕方はあるようですが、さるの場合はアロー関数表記を使って、引数込みのメソッド・コールとしました。
onPress={this.startPlaying(item)} → onPress={()=>this.startPlaying(item)}
これが、一番気楽だったから。


3)SoundPlayer.playXxxx前にコールバック指定はしておくこと
react-native-sound-playerplaySoundFile()playUrl()を実行する前には、
SoundPlayer.addEventListener('FinishedPlaying' ・・・);
を実施しておく必要がありそうだ。
そんなことは、本家サイトに書いてたか分かってましたが、
addEventListenerをする前に、setState()をやっちゃって、かつ2)onPress={this.startPlaying(item)}を指定していたお蔭で、コールバック未指定の状態で、playSoundFile()が実行されて、Exceptionが発生してました。
以下のような、メッセージがエミュレータ画面上に表示されます。

Attempt to invoke virtual method 'void android.media.MediaPlayer.setOnCompletionListener(android.media,MediaPlayer$OnCompletionListener)' on null object reference



componentDidMount内でのsetState()をメソッドの最後に実行するようにした。
ただし、※その後1)の件があって、Fs.readDir(this.dirpath).then()に移動。


4)それでもException(Attempt to invoke・・・)が出る
上記した、Exceptionのエラー現象が消えませんでした。

エラーメッセージをキーにして検索すると同様の事象が発生したらしい報告がありました。
その例ではplaySoundFile()に渡す「fileName」に指定するファイル名が間違っていたようなことが書かれていた。
(ファイル名を間違えるとsetOnCompletionListenerでエラー?)

もう一度、本家のサイトの説明を見てみました。
参考:https://www.npmjs.com/package/react-native-sound-player
tone.mp3というファイルを再生するサンプルコードが載ってます。
playSoundFile(fileName, fileType)をコールしろとあって、「tone」と「mp3」を分離して指定してます。

なぜ、拡張子部分を分離するのか?ちょっと違和感がありました。
さるは、フルパスのファイル名の最後の「.m4a」を取り除いたものを「fileName」パラメタにしてしていました。

先のサイトのUsageには、先に音声ファイルを「{project_root}/android/app/src/main/res/raw/」に入れる手順が書かれています。
読み流していたけど、これってフォルダ限定ということ?

react-native-sound-playerのソースを追っかけて見ました。
すると、異常終了している箇所のオリジナルコードは、こうです。
「(プロジェクトフォルダ)\node_modules\react-native-sound-player\android\src\main\java\com\johnsonsu\rnsoundplayer\」
----RNSoundPlayerModule.java-----------------------

private void mountSoundFile(String name, String type) throws IOException {
if (this.mediaPlayer == null) {
int soundResID = getReactApplicationContext().getResources().getIdentifier(name, "raw", getReactApplicationContext().getPackageName());

if (soundResID > 0) {
this.mediaPlayer = MediaPlayer.create(getCurrentActivity(), soundResID);
} else {
this.mediaPlayer = MediaPlayer.create(getCurrentActivity(), this.getUriFromFile(name, type));
}

this.mediaPlayer.setOnCompletionListener( //←ここでエラー
new OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer arg0) {
WritableMap params = Arguments.createMap();
params.putBoolean("success", true);
sendEvent(getReactApplicationContext(), EVENT_FINISHED_PLAYING, params);
}
});
} else {
:
---------------------------------------------------

直前の処理の結果が正常かどうか(this.mediaPlayer==正常値)を確認せずに処理を続けちゃってる雑な感じのコードではありますが。
いずれ、その前の2種類のMediaPlayer.createが失敗しているのが原因でしょう。

さらにその前のsoundResIDを求める処理は何だ?
ここら辺は、以下のAndroidのMediaPlayerの説明を読むしかありません。
参考:https://developer.android.com/reference/android/media/MediaPlayer?hl=ja
create()の説明を見ると、パラメタの組み合わせの違いで3種類の呼び出し方法がある。
その内第2パラメタがresidのものがある。

RNSoundPlayerModule.javaでは、そのresidが取得できなければ、URIを求めて、createを実行する流れになっている。
どっちに流れてるんだろう。ともかくデフォルトの処理的には、residを使う側っぽいので、getIdentifier()について検索してみた。
参考:https://developer.android.com/reference/android/content/res/Resources?hl=ja
参考:https://developer.android.com/guide/topics/providers/content-provider-basics?hl=ja
が、どんどん深みにはまりそうだったので読むのをやめた。

residの取得に失敗した場合のURIを使う方法に真っすぐ行く方法はないのか?

RNSoundPlayerModule.java内にplayUrl(String url)というメソッドがある。
それを使った場合には、prepareUrl()がコールされていて、その中でUri.parse(url)でURLをURIに変換して、それを使ってMediaPlayer.create()している。
Uriクラスの仕様は?URLとURIの違いって何?
参考:https://developer.android.com/reference/android/net/Uri
参考:https://qiita.com/wakamesoba98/items/98b79bdfde19612d12b0

ファイルパスに「file://」を付ければ、行けんじゃね?


PlayList.jsの「SoundPlayer.playSoundFile(filepath_noext, "m4a");」の部分を
「let audioUri = "file://" + item.path; SoundPlayer.playUrl(audioUri);」
に変更してみました。
再生ができました。


5)連続再生させられない
喜んだのもつかの間、2つ目のファイルの再生ができません。
(音が出ません。)
それでも、画面上のボタンをクリックし続けたらエラーになりました。

Cloud not invoke
RNSoundPlayer.playUrl
null
setDataSourceFD failed
status=0x800000


まずは、2回目のボタンクリックでなぜ再生されないのかを調べます。
RNSoundPlayerModule.javaと本家MesdiaPlayerの説明(特に状態遷移図)を見比べながら何か変なところは無いのかさるなりに確認してみました。
でも・・・良さげです。

実際の動きを見るために、あっちこっちにlogcat用のLog.d(・・・・・);を入れて見てみました。

RNSoundPlayerModule.java内でのシーケンスで、1回目のときはpreparedになってから、再生が終わってcompletedになるのですが、
2回目以降は、completedが先に来て、その後preparedが出ます。
明らかにおかしい。

.javaのコード上で使っているAPIが、2回目以降はprepare()ではなくprepareAsync()になってます。
外部をアクセスする(時間がかかる)可能性があるために非同期なんだろうとは思ってました。
当然その辺の同期対策は整っているかと想像してたのですが、そうではないようです。


試しに、prepareAsync()prepare()に変更して試してみました。
うまく動作しました。複数の録音を再生できます。

最初に感じた雑さがここにもあったということでしょうか。
それとも、さるコード(PlayList.js)のフレームワークの使い方が悪いのか?
ともかく、この修正はこのままにしました。


6)サイズ0のファイルでException
最初に気になった「MesiaPlayer.create()」に失敗っしたときの継続可否チェックを行っていない点も修正しはじめた。
ちょっと不安ですが、動かないんだから致し方なしです。

これで結構困ったのが、ネイティブモジュール(Java)側からアプリ(JavaScript)側への結果の通知の仕方です。
Javaの外向きの関数はすべてvoidで、非同期で実行されるんだとか。
それで、「return XXXX;」とかやっても無効なんだそうです。

えーそうなのー。ここにきて初めて知りました。
じゃあどうやって処理結果をコール元に伝えるんだ?

コールバック等するしかないんだそうです。
参考:https://blog.mitsuruog.info/2019/02/react-native-native-module-android
上記で説明してくれている中で、一番楽そうな方法を選びました。

ただし、ここではコールバックを登録したら即「invoke()」していることが、さる的にかなりの混乱の元になりました。
そうにしかできないんだろうか・・・・

試してみたら、コールバックをメンバに保存してけば、後でinvokeすることもできた。

さらに混乱の元になったのが、invoke自体が同期ではなく非同期でコールバックする点でした。

この辺、JavaScriptからNative(Java)の「playUrl()」の実行結果を同期で受け取ろうとしてかなりカット&トライしてみました。
でも、どうにもピッタリくるやり方がわからなくて、結局、非同期になってしまうものは非同期で処理を流すことに。

それと、コールバック登録してinvokeするやり方は、invokeした後は無効で、1回しか使えないんだそうです。
なんで~?・・・でも「そうだ」と言うならしかたない。
なので、PlayList.js側で、「playUrl()」する度に、「setCallbackForOpen()」を毎回コールしています。

テストしている内に
ファイルは存在するけど、サイズ0のファイルを2回目以降に再生しようとするとExceptionが発生することに気づきました。

発生しているのは、2つ目のファイルのときに実行している「MesiaPlayer.setDataSource()」のところです。
元々この関数にreturnはありません。なのでファイルアクセスに失敗すると、IOExceptionになります。

これに対して、react-native-sound-playerパッケージのNative側(Java)は、Exceptionをthrowして済まそうとしている感じです。
だけど、JavaScript側でも、それを拾ってやろうとはしてない。
チグハグというか、なんか無責任、それとも未完成?


ともかく、動かないんだからしょうがないので、直します。

結構な修正になってしまったので、全文載せます。
細かい修正の意味は上記のボヤキから類推願います。
「(プロジェクトフォルダ)\node_modules\react-native-sound-player」
----index.js----------------------------------------
/**
* @flow
*/
'use strict'

import { NativeModules, NativeEventEmitter, Platform } from 'react-native'
const { RNSoundPlayer } = NativeModules

const _soundPlayerEmitter = new NativeEventEmitter(RNSoundPlayer)
let _finishedPlayingListener = null
let _finishedLoadingListener = null

export default {

playSoundFile: (name: string, type: string) => {
RNSoundPlayer.playSoundFile(name, type)
},

loadSoundFile: (name: string, type: string) => {
RNSoundPlayer.loadSoundFile(name, type)
},

playUrl: (url: string) => {
RNSoundPlayer.playUrl(url);
//console.log("index.js playUrl end:");
},

loadUrl: (url: string) => {
RNSoundPlayer.loadUrl(url)
},

//# Temporary: The notice to the native module of the callback.
setCallbackForOpen: (callback: Function) => {
RNSoundPlayer.setCallbackForOpen(callback);
//console.log("index.js set callback.");
},


onFinishedPlaying: (callback: (success: boolean) => any) => {
if (_finishedPlayingListener) {
_finishedPlayingListener.remove()
_finishedPlayingListener = undefined
}

_finishedPlayingListener = _soundPlayerEmitter.addListener(
'FinishedPlaying',
callback
)
},

onFinishedLoading: (callback: (success: boolean) => any) => {
if (_finishedLoadingListener) {
_finishedLoadingListener.remove()
_finishedLoadingListener = undefined
}

_finishedLoadingListener = _soundPlayerEmitter.addListener(
'FinishedLoading',
callback
)
},

addEventListener: (eventName: 'FinishedLoading' | 'FinishedPlaying' | 'FinishedLoadingURL' | 'FinishedLoadingFile', callback: Function) => _soundPlayerEmitter.addListener(eventName, callback),

play: () => {
// play and resume has the exact same implementation natively
RNSoundPlayer.resume()
},

pause: () => {
RNSoundPlayer.pause()
},

resume: () => {
RNSoundPlayer.resume()
},

stop: () => {
RNSoundPlayer.stop()
},

seek: (seconds: number) => {
RNSoundPlayer.seek(seconds)
},

setVolume: (volume: number) => {
RNSoundPlayer.setVolume(volume)
},

setSpeaker: (on: boolean) => {
if(Platform.OS === "android"){
console.log("setSpeaker is not implement on Android");
} else {
RNSoundPlayer.setSpeaker(on);
}
},

getInfo: async () => RNSoundPlayer.getInfo(),

unmount: () => {
if (_finishedPlayingListener) {
_finishedPlayingListener.remove()
_finishedPlayingListener = undefined
}

if (_finishedLoadingListener) {
_finishedLoadingListener.remove()
_finishedLoadingListener = undefined
}
}
}
---------------------------------------------------

「(プロジェクトフォルダ)\node_modules\react-native-sound-player\android\src\main\java\com\johnsonsu\rnsoundplayer」
----RNSoundPlayerModule.java-----------------------
package com.johnsonsu.rnsoundplayer;

import android.media.MediaPlayer;
import android.media.MediaPlayer.OnCompletionListener;
import android.media.MediaPlayer.OnPreparedListener;
import android.net.Uri;
import android.util.Log;
import java.io.File;

import java.io.IOException;
import javax.annotation.Nullable;

import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Promise;


public class RNSoundPlayerModule extends ReactContextBaseJavaModule {

public final static String TAG = "RNSoundPlayerModule";
public final static String EVENT_FINISHED_PLAYING = "FinishedPlaying";
public final static String EVENT_FINISHED_LOADING = "FinishedLoading";
public final static String EVENT_FINISHED_LOADING_FILE = "FinishedLoadingFile";
public final static String EVENT_FINISHED_LOADING_URL = "FinishedLoadingURL";

private final ReactApplicationContext reactContext;
private MediaPlayer mediaPlayer;
private float volume;

public RNSoundPlayerModule(ReactApplicationContext reactContext) {
super(reactContext);
this.reactContext = reactContext;
this.volume = 1.0f;
}

@Override
public String getName() {
return "RNSoundPlayer";
}

//# Show success of the MediaPlayer.create(). Back to App side.
private boolean fCreated = false;


@ReactMethod
//public void playSoundFile(String name, String type) throws IOException {
public void playSoundFile(String name, String type) {
try {
this.fCreated = mountSoundFile(name, type);
if (this.fCreated) {
this.resume();
}
}
catch (IOException e) {
this.fCreated = false;
Log.d(TAG, "playSoundFile: caught the IOException!");
}
finally {
this.notifyCreated.invoke(this.fCreated);
}
}


@ReactMethod
//public void loadSoundFile(String name, String type) throws IOException {
public void loadSoundFile(String name, String type) {
try {
this.fCreated = mountSoundFile(name, type);
}
catch (IOException e) {
this.fCreated = false;
Log.d(TAG, "loadSoundFile: caught the IOException!");
}
finally {
this.notifyCreated.invoke(this.fCreated);
}
}


@ReactMethod
//public void playUrl(String url) throws IOException {
public void playUrl(String url) {
try {
this.fCreated = prepareUrl(url);
//Log.d(TAG, "notify of creation of player :"+this.fCreated);
if (this.fCreated) {
this.resume();
}
}
catch (IOException e) {
this.fCreated = false;
Log.d(TAG, "playUrl: caught the IOException!");
}
finally {
this.notifyCreated.invoke(this.fCreated);
}
}


@ReactMethod
//public void loadUrl(String url) throws IOException {
public void loadUrl(String url) {
try {
this.fCreated = prepareUrl(url);
}
catch (IOException e) {
this.fCreated = false;
Log.d(TAG, "loadUrl: caught the IOException!");
}
finally {
this.notifyCreated.invoke(this.fCreated);
}
}


//# Return a result the MediaPlayer.create().
private Callback notifyCreated = null;
@ReactMethod
public void setCallbackForOpen(Callback callback) {
this.notifyCreated = callback;
//Log.i(TAG, "set notifyCreated.");
}


@ReactMethod
public void pause() throws IllegalStateException {
if (this.mediaPlayer != null) {
this.mediaPlayer.pause();
}
}

@ReactMethod
public void resume() throws IOException, IllegalStateException {
if (this.mediaPlayer != null) {
this.setVolume(this.volume);
this.mediaPlayer.start();
}
}

@ReactMethod
public void stop() throws IllegalStateException {
if (this.mediaPlayer != null) {
this.mediaPlayer.stop();
}
}

@ReactMethod
public void seek(float seconds) throws IllegalStateException {
if (this.mediaPlayer != null) {
this.mediaPlayer.seekTo((int)seconds * 1000);
}
}

@ReactMethod
public void setVolume(float volume) throws IOException {
this.volume = volume;
if (this.mediaPlayer != null) {
this.mediaPlayer.setVolume(volume, volume);
}
}

@ReactMethod
public void getInfo(
Promise promise) {
WritableMap map = Arguments.createMap();
map.putDouble("currentTime", this.mediaPlayer.getCurrentPosition() / 1000.0);
map.putDouble("duration", this.mediaPlayer.getDuration() / 1000.0);
promise.resolve(map);
}

private void sendEvent(ReactApplicationContext reactContext,
String eventName,
@Nullable WritableMap params) {
reactContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit(eventName, params);
}

private boolean mountSoundFile(String name, String type) throws IOException {
if (this.mediaPlayer == null) {
int soundResID = getReactApplicationContext().getResources().getIdentifier(name, "raw", getReactApplicationContext().getPackageName());
//# If the file path is invalid or the file data is invalid, a null is returned.
if (soundResID > 0) {
this.mediaPlayer = MediaPlayer.create(getCurrentActivity(), soundResID);
} else {
this.mediaPlayer = MediaPlayer.create(getCurrentActivity(), this.getUriFromFile(name, type));
}
//# Evade the end of the unnecessary exception.
if (this.mediaPlayer == null) {
return false;
}

this.mediaPlayer.setOnCompletionListener(
new OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer arg0) {
WritableMap params = Arguments.createMap();
params.putBoolean("success", true);
sendEvent(getReactApplicationContext(), EVENT_FINISHED_PLAYING, params);
}
});
} else {
Uri uri;
int soundResID = getReactApplicationContext().getResources().getIdentifier(name, "raw", getReactApplicationContext().getPackageName());

if (soundResID > 0) {
uri = Uri.parse("android.resource://" + getReactApplicationContext().getPackageName() + "/raw/" + name);
} else {
uri = this.getUriFromFile(name, type);
}
this.mediaPlayer.reset();
//# If the file don't exist or included any invalid data, an exception occurre.
//# In that case, the exception is caught by the caller.

this.mediaPlayer.setDataSource(getCurrentActivity(), uri);
this.mediaPlayer.prepare();
}

WritableMap params = Arguments.createMap();
params.putBoolean("success", true);
sendEvent(getReactApplicationContext(), EVENT_FINISHED_LOADING, params);
WritableMap onFinishedLoadingFileParams = Arguments.createMap();
onFinishedLoadingFileParams.putBoolean("success", true);
onFinishedLoadingFileParams.putString("name", name);
onFinishedLoadingFileParams.putString("type", type);
sendEvent(getReactApplicationContext(), EVENT_FINISHED_LOADING_FILE, onFinishedLoadingFileParams);
return true;
}

private Uri getUriFromFile(String name, String type) {
String folder = getReactApplicationContext().getFilesDir().getAbsolutePath();
String file = name + "." + type;

// http://blog.weston-fl.com/android-mediaplayer-prepare-throws-status0x1-error1-2147483648
// this helps avoid a common error state when mounting the file
File ref = new File(folder + "/" + file);

if (ref.exists()) {
ref.setReadable(true, false);
}

return Uri.parse("file://" + folder + "/" + file);
}

private boolean prepareUrl(final String url) throws IOException {
if (this.mediaPlayer == null) {
Uri uri = Uri.parse(url);
//# If the file path is invalid or the file data is invalid, a null is returned.
this.mediaPlayer = MediaPlayer.create(getCurrentActivity(), uri);
//# Evade the end of the unnecessary exception.
if (this.mediaPlayer == null) {
return false;
}

this.mediaPlayer.setOnCompletionListener(
new OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer arg0) {
Log.d(TAG, "completed.");
WritableMap params = Arguments.createMap();
params.putBoolean("success", true);
sendEvent(getReactApplicationContext(), EVENT_FINISHED_PLAYING, params);
}
});
this.mediaPlayer.setOnPreparedListener(
new OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mediaPlayer) {
Log.d(TAG, "prepared.");
WritableMap onFinishedLoadingURLParams = Arguments.createMap();
onFinishedLoadingURLParams.putBoolean("success", true);
onFinishedLoadingURLParams.putString("url", url);
sendEvent(getReactApplicationContext(), EVENT_FINISHED_LOADING_URL, onFinishedLoadingURLParams);
}
}
);
} else {
Uri uri = Uri.parse(url);
Log.d(TAG, "uri="+uri+", mediaPlayer="+this.mediaPlayer);
this.mediaPlayer.reset();
//# If the file don't exist or included any invalid data, an exception occurre.
//# In that case, the exception is caught by the caller.

this.mediaPlayer.setDataSource(getCurrentActivity(), uri);
//# Because the call of the async function here is not handled appropriately, switched to sync function.
//this.mediaPlayer.prepareAsync();
this.mediaPlayer.prepare();

}
WritableMap params = Arguments.createMap();
params.putBoolean("success", true);
sendEvent(getReactApplicationContext(), EVENT_FINISHED_LOADING, params);
return true;
}
}
---------------------------------------------------



7)再生中のボタンの背景色が変わらない
最後に再生中の表示についてです。

再生中の音声ファイル名ボタンの背景色を変えるようにコード(style={styles.Xxxxx})を書いたつもりでしたが、何の変化も見られません。
参考:https://bagelee.com/programming/react-native/button-react-native/

ReactNative本家のボタンコンポーネントの説明を見ます。
参考:http://reactnative.dev/docs/button.html

すると、「style」という属性に関する記述が見つかりません。
「color」はある。
そーなのー?

ともかく、「color='#xxxxxx'」に変更してみたら背景色が変わりました。


以上の作成・修正コードで、以下のような画面出ます。
20200416_1.jpg
再生中の表示です。

別に、パッケージの仕様を変更したいとか思ってたわけじゃなくて、そのまま使えたらハッピーだと思ってたのですが・・・
こんなに修正することになろうとは、思ってもいませんでした。

特にNativeモジュール側の処理結果をJavaScript側に伝えるところが、未だに分けわかんない。
今回使い捨てのコールバック登録の方法を使いましたが、本来はNativeEventEmitterとかDeviceEventEmitterとかを使うものらしいです。
ここ、要注意なのですが、イベントを発生させる側(Nativeモジュール)をしっかり勉強しないといけなくなりそうなので、先送りさせていただきます。

本当は、再生中のポップアップ表示とか、一時停止/再スタートとかの制御もやってみたかったのだけど・・・
ちょっと、最終目標の全体像の構成の準備を急ぎます。

次回は、今現在のアプリを実機に載せて試すところをやってみようかと思ってます。
スマフォを手に入れないといけないけどね。

では、この回はこの辺で。ご機嫌よう。
m(__)m
スポンサーサイト





コメントの投稿

非公開コメント

カレンダー
01 | 2024/02 | 03
- - - - 1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 - -
プロフィール

さるもすなる

Author:さるもすなる
さるです。別HPサイト「さるもすなる」から侵食してきました。 山菜/きのこ、それとタイトルにしたPPバンド籠のことをメインに徒然に・・・・暇を持て余したさるの手仕事:男手芸のブログってことで。

最新記事
最新コメント
月別アーカイブ
カテゴリ
天気予報

-天気予報コム- -FC2-
本家のHPのトップ
山菜や茸の話です
PPバンドの籠作品と作り方です
投稿をお待ちしております



PVアクセスランキング にほんブログ村 にほんブログ村 ハンドメイドブログへ



マニュアルのお申し込み



検索フォーム
リンク
RSSリンクの表示
ブロとも申請フォーム

この人とブロともになる

QRコード
QR