Android聲音相關總結

不洗碗工作室發表於2018-04-12

作者:不洗碗工作室 - lszr

文章出處:Android聲音相關總結

版權歸作者所有,轉載請註明出處

1、控制系統聲音

Android中系統聲音可以直接去控制,不需要獲取系統許可權,使用方法也很簡單,下面看例子:

public class VolumeControlClass extends AppCompatActivity {
    public void changeVolume(){
        //宣告AudioManager物件並初始化
        AudioManager audioManager=(AudioManager) getSystemService(Context.AUDIO_SERVICE);

        //控制鬧鐘音量
        audioManager.setStreamVolume(AudioManager.STREAM_ALARM,AudioManager.ADJUST_LOWER,0);
        //控制DTMF音調的聲音
        audioManager.setStreamVolume(AudioManager.STREAM_DTMF,0,0);
        //媒體聲音
        audioManager.setStreamVolume(AudioManager.STREAM_MUSIC,0,0);
        //系統提示音
        audioManager.setStreamVolume(AudioManager.STREAM_NOTIFICATION,0,0);
        //電話鈴聲
        audioManager.setStreamVolume(AudioManager.STREAM_RING,0,0);
        //系統音量
        audioManager.setStreamVolume(AudioManager.STREAM_SYSTEM,0,0);
        //通話語音音量
        audioManager.setStreamVolume(AudioManager.STREAM_VOICE_CALL,0,0);
    }
}
複製程式碼
  • 引數說明:

    第一個引數用來指定控制的相關的音量型別,即什麼的音量,第二個引數用來指定音量的大小或者說調整方向,第三個引數是附加引數,有如下幾個引數:

    Android聲音相關總結

    FLAG_ALLOW_RINGER_MODES 在更改音量時是否包含鈴聲模式作為可能的選項。

    FLAG_PLAY_SOUND 更改音量時是否播放聲音。

    FLAG_REMOVE_SOUND_AND_VIBRATE 刪除可能在佇列中或正在播放的任何聲音/振動(與更改音量有關)。

    FLAG_SHOW_UI 顯示包含當前音量的Toast。

    FLAG_VIBRATE 如果進入振動振鈴模式,是否振動。

2、播放音訊

一、使用MediaPlayer播放本地音訊

Android中播放音訊可以用MediaPlayer進行音訊和視訊的播放,這裡先說播放本地音訊的方法:

首先要進行許可權請求

//判斷是否有閱讀快取的許可權
if (ContextCompat.checkSelfPermission(playMusicActivity.this, 
									Manifest.permission.WRITE_EXTERNAL_STORAGE) 
									== PackageManager.PERMISSION_GRANTED) {
		//初始化本地播放
        initPlay();
    } else {
        ActivityCompat.requestPermissions(
            playMusicActivity.this, 
            new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 
            1);
    }
複製程式碼

同時要在manifest中加入:

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
複製程式碼

作為許可權申請。,下面是播放本地視訊的初始化條件

/**
 * 初始化本地播放
 */
private void initPlay() {
    try {
    	//獲取file物件
        File file = new File(Environment.getExternalStorageDirectory(), "music.mp3");
		//設定對應的檔案路徑
        mMediaPlayer.setDataSource(file.getPath());
      //播放準備
        mMediaPlayer.prepare();
    } catch (IOException e) {
        e.printStackTrace();
    }

}
複製程式碼

能夠看到,MediaPlayer物件在初始化時應該設定播放路徑,然後呼叫prepare()方法或者prepareAsync()方法,來進行初始化,也就是播放準備,這兩個方法區別在於是否非同步,prepareAsync()方法是非同步的,在播放網路視訊中一般呼叫這個方法來初始化。

Android聲音相關總結

這是MediaPlayer的生命週期,關於其中的一些解釋如下:

Idle 狀態:當使用new()方法建立一個MediaPlayer物件或者呼叫了其reset()方法時,該MediaPlayer物件處於idle狀態。這兩種方法的一個重要差別就是:如果在這個狀態下呼叫了getDuration()等方法(相當於呼叫時機不正確),通過reset()方法進入idle狀態的話會觸發OnErrorListener.onError(),並且MediaPlayer會進入Error狀態;如果是新建立的MediaPlayer物件,則並不會觸發onError(),也不會進入Error狀態。

End 狀態:通過release()方法可以進入End狀態,只要MediaPlayer物件不再被使用,就應當儘快將其通過release()方法釋放掉,以釋放相關的軟硬體元件資源,這其中有些資源是隻有一份的(相當於臨界資源)。如果MediaPlayer物件進入了End狀態,則不會在進入任何其他狀態了。

Initialized 狀態:這個狀態比較簡單,MediaPlayer呼叫setDataSource()方法就進入Initialized狀態,表示此時要播放的檔案已經設定好了。

Prepared 狀態:初始化完成之後還需要通過呼叫prepare()或prepareAsync()方法,這兩個方法一個是同步的一個是非同步的,只有進入Prepared狀態,才表明MediaPlayer到目前為止都沒有錯誤,可以進行檔案播放。

Preparing 狀態:這個狀態比較好理解,主要是和prepareAsync()配合,如果非同步準備完成,會觸發OnPreparedListener.onPrepared(),進而進入Prepared狀態。

Started 狀態:顯然,MediaPlayer一旦準備好,就可以呼叫start()方法,這樣MediaPlayer就處於Started狀態,這表明MediaPlayer正在播放檔案過程中。可以使用isPlaying()測試MediaPlayer是否處於了Started狀態。如果播放完畢,而又設定了迴圈播放,則MediaPlayer仍然會處於Started狀態,類似的,如果在該狀態下MediaPlayer呼叫了seekTo()或者start()方法均可以讓MediaPlayer停留在Started狀態。

Paused 狀態:Started狀態下MediaPlayer呼叫pause()方法可以暫停MediaPlayer,從而進入Paused狀態,MediaPlayer暫停後再次呼叫start()則可以繼續MediaPlayer的播放,轉到Started狀態,暫停狀態時可以呼叫seekTo()方法,這是不會改變狀態的。

Stop 狀態:Started或者Paused狀態下均可呼叫stop()停止MediaPlayer,而處於Stop狀態的MediaPlayer要想重新播放,需要通過prepareAsync()和prepare()回到先前的Prepared狀態重新開始才可以。

PlaybackCompleted狀態:檔案正常播放完畢,而又沒有設定迴圈播放的話就進入該狀態,並會觸發OnCompletionListener的onCompletion()方法。此時可以呼叫start()方法重新從頭播放檔案,也可以stop()停止MediaPlayer,或者也可以seekTo()來重新定位播放位置。

Error狀態:如果由於某種原因MediaPlayer出現了錯誤,會觸發OnErrorListener.onError()事件,此時MediaPlayer即進入Error狀態,及時捕捉並妥善處理這些錯誤是很重要的,可以幫助我們及時釋放相關的軟硬體資源,也可以改善使用者體驗。通過setOnErrorListener(android.media.MediaPlayer.OnErrorListener)可以設定該監聽器。如果MediaPlayer進入了Error狀態,可以通過呼叫reset()來恢復,使得MediaPlayer重新返回到Idle狀態。

在初始化完成後,在button監聽中寫下對應的開始,暫停,停止播放的操作,即呼叫start(),pause(),stop()方法:

public void playLocalMusicListener() {
    mBtnStart.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
        		//開始播放
            mMediaPlayer.start();
        }
    });
    mBtnSuspend.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
        		//暫停播放
            mMediaPlayer.pause();
        }
    });
    mBtnStop.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
        		//停止播放
            mMediaPlayer.stop();
            //停止後重新prepare()
            try {
                mMediaPlayer.prepare();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    });
}
複製程式碼

可以看到,在呼叫stop()方法後需要重新呼叫prepare()方法,否則會導致無法重新播放。 如上就是使用MediaPlayer播放本地音訊的方法。

二、使用MediaPlayer播放網路音訊

與播放本地音訊不同的是,播放網路視訊的時候一般需要進行非同步的prepare,因此,需要實現四個介面:

	MediaPlayer.OnCompletionListener,
	MediaPlayer.OnErrorListener,
    MediaPlayer.OnBufferingUpdateListener,
    MediaPlayer.OnPreparedListener
複製程式碼

這些方法分別為播放完成,錯誤,緩衝中,緩衝完成的回撥方法,在類中這樣去重寫

/**
 * 播放完音訊後呼叫的方法
 *
 * @param mp
 */
@Override
public void onCompletion(MediaPlayer mp) {
    mBtnNetworkStart.setEnabled(false);
    mBtnNetworkPause.setEnabled(false);
    mBtnNetworkStop.setEnabled(false);
    mMediaPlayerNetwork.prepareAsync();
}


@Override
public boolean onError(MediaPlayer mp, int what, int extra) {

    return false;
}

/**
 * 緩衝中呼叫的方法
 *
 * @param mp
 * @param percent
 */
@Override
public void onBufferingUpdate(MediaPlayer mp, int percent) {

}


/**
 * 播放遠端音訊時快取完成後的操作
 *
 * @param mp
 */
@Override
public void onPrepared(MediaPlayer mp) {
    mBtnNetworkStart.setEnabled(true);
    mBtnNetworkPause.setEnabled(false);
    mBtnNetworkStop.setEnabled(false);

}
複製程式碼

這些方法中我根據按鈕點選的邏輯設定了不同的按鈕的可點選與不可點選事件的方法。 同樣的,播放網路音訊也需要像本地音訊一樣的初始化,程式碼如下:

/**
 * 初始化網路音訊播放
 */
public void initNetworkPlay() {
    mMediaPlayerNetwork.setOnCompletionListener(this);
    mMediaPlayerNetwork.setOnErrorListener(this);
    mMediaPlayerNetwork.setOnBufferingUpdateListener(this);
    mMediaPlayerNetwork.setOnPreparedListener(this);
    Uri mUri = Uri.parse("http://music.163.com/song/media/outer/url?id=317151.mp3");
    try {
        mMediaPlayerNetwork.setDataSource(this, mUri);
        mMediaPlayerNetwork.prepareAsync();
        mBtnNetworkStart.setEnabled(false);
        mBtnNetworkPause.setEnabled(false);
        mBtnNetworkStop.setEnabled(false);

    } catch (IOException e) {
        e.printStackTrace();
    }
}
複製程式碼

首先要和四個實現的方法進行繫結,然後呼叫setDataSource()方法,但是之後要呼叫非同步的prepareAsync()。在呼叫prepareAsync()方法時,需要在對應的緩衝中方法和緩衝完成的方法中呼叫對應的指令,我在這裡是在緩衝完成的回撥中是開始播放按鈕可點選,暫停和停止不可點選。如下的按鈕繫結事件也是有類似的邏輯:

/**
 * 繫結播放網路音訊監聽
 */
public void playNetworkMusicListener() {
    mBtnNetworkStart.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            mMediaPlayerNetwork.start();
            mBtnNetworkPause.setEnabled(true);
            mBtnNetworkStop.setEnabled(true);
        }

    });

    mBtnNetworkPause.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            if (mMediaPlayerNetwork.isPlaying()) {
                mBtnNetworkPause.setEnabled(false);

                mMediaPlayerNetwork.pause();
            }
        }
    });

    mBtnNetworkStop.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            mMediaPlayerNetwork.stop();
            mBtnNetworkStart.setEnabled(false);
            mBtnNetworkPause.setEnabled(false);
            mBtnNetworkStop.setEnabled(false);
            mMediaPlayerNetwork.prepareAsync();

        }
    });
}
複製程式碼

如上就是使用MediaPlayer播放網路音訊的方法

三、使用SoundPool播放音訊

SoundPool一般用來 播放密集,急促而又短暫的音效,比如特技音效:Duang~,遊戲用得較多,這裡根據網上的教程,使用老式的SoundPool的構造方法是可以直接用的,老式的構造方法有三個引數,第一個為指定支援多少個聲音,SoundPool物件中允許同時存在的最大流的數,第二個為指定聲音型別,流型別可以分為STREAM_VOICE_CALL, STREAM_SYSTEM, STREAM_RING,STREAM_MUSIC 和 STREAM_ALARM四種型別,可以指定使用不同型別的系統音量去播放音效,比如通話音量和媒體音量,在AudioManager中定義,第三個為指定聲音品質(取樣率變換質量),一般直接設定為0。

使用SoundPool一般都會使用HashMap來進行音效和id之間的對應關係,同時在put進map的時候直接呼叫load方法進行載入:

	HashMap<Integer, Integer> musicId = new HashMap<Integer, Integer>();

    //通過load方法載入指定音訊流,並將返回的音訊ID放入musicId中

    musicId.put(1, sp.load(this, R.raw.first, 0));

    musicId.put(2, sp.load(this, R.raw.second, 0));

    musicId.put(3, sp.load(this, R.raw.third, 0));
複製程式碼

然後在按鈕的點選事件中呼叫play方法來播放音訊,關於play方法,有幾個不同的引數:

play(int soundID, float leftVolume, float rightVolume, int priority, int loop, float rate)
複製程式碼

soundID就是load返回的id內容,第二三個引數為左右聲道的音量,範圍為0.0-1.0,第四個引數指定播放聲音的優先順序,數值越高,優先順序越大,loop指定是否迴圈,-1表示無限迴圈,0表示不迴圈,其他值表示要重複播放的次數,rate是指定播放速率:1.0的播放率可以使聲音按照其原始頻率,而2.0的播放速率,可以使聲音按照其 原始頻率的兩倍播放。如果為0.5的播放率,則播放速率是原始頻率的一半。播放速率的取值範圍是0.5至2.0。

但是這個老式的構造方法在api21以後已經被廢棄了,官方建議使用SoundPool.Builder這個靜態內部類的構造方法:

use SoundPool.Builder instead to create and configure a SoundPool instance

我根據官方的文件進行使用時,剛開始無論如何都無法播放聲音,換成老式的構造方法則可以播放,於是去看原始碼:

public SoundPool build() {
        if (mAudioAttributes == null) {
            mAudioAttributes = new AudioAttributes.Builder()
                    .setUsage(AudioAttributes.USAGE_MEDIA).build();
        }
        return new SoundPool(mMaxStreams, mAudioAttributes);
    }
複製程式碼

之前一直都是呼叫的這個方法來進行呼叫load方法和play方法,可以看到,這個方法每次呼叫的時候都會建立一個新的物件,所以肯定沒有辦法播放,這時再用SoundPool去宣告一個新的物件等於前面那個物件呼叫build()方法:

	mSoundPool = new SoundPool.Builder();
		//設定最大播放數目
    mSoundPool.setMaxStreams(4);
    sp=mSoundPool.build();
複製程式碼

這裡使用sp去呼叫load和play方法就可以正常地播放了。

3、Android的三個錄音方法

一、呼叫系統錄音功能直接錄音:

使用Intent呼叫系統本身自帶的錄音功能可以實現簡單的錄音功能:

//系統錄音功能(開始)
    btnStartRecord.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Intent intent = new Intent(MediaStore.Audio.Media.RECORD_SOUND_ACTION);
            startActivityForResult(intent,1);
        }
    });
複製程式碼

但是注意要在manifest中寫好permission:

<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
複製程式碼

然後在回撥方法中通過Cursor獲取檔案路徑,然後使用MediaPlayer進行播放:

/**
 * 回撥錄音完畢內容
 * @param requestCode
 * @param resultCode
 * @param data
 */
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {

    switch (requestCode) {
        case 1:
            try {
                Uri uri = data.getData();
                Log.d("test2", String.valueOf(uri));

                String filePath = getAudioFilePathFromUri(uri);
                filePathOfMusic=filePath;
                mMediaPlayer.setDataSource(filePathOfMusic);
                mMediaPlayer.prepare();
                Log.d("test", filePath);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
            break;
        default:
            break;
    }
}
複製程式碼

呼叫系統錄音功能較為簡單,下面看呼叫官方類庫的方法:

二、呼叫MediaRecorder類的方法來錄音:

總的來說,使用MediaRecorder流程如下:

1、建立MediaRecorder物件。

2、呼叫MediaRecorder物件的setAudioSource()方法設定聲音來源,一般傳入MediaRecorder.AudioSource.MIC引數指定錄製來自麥克風的聲音。

3、呼叫MediaRecorder物件的setOutputFormat()設定所錄製的音訊檔案的格式。

4、呼叫MediaRecorder物件的setAudioEncoder()、setAudioEncodingBitRate(int bitRate)、setAudioSamplingRate(int samplingRate)設定所錄製的聲音的編碼格式、編碼位率、取樣率等,這些引數將可以控制所錄製的聲音的品質、檔案的大小。一般來說,聲音品質越好,聲音檔案越大。

5、呼叫MediaRecorder的setOutputFile(String path)方法設定錄製的音訊檔案的儲存位置。

6、呼叫MediaRecorder的prepare()方法準備錄製。

7、呼叫MediaRecorder物件的start()方法開始錄製。

8、錄製完成,呼叫MediaRecorder物件的stop()方法停止錄製,並呼叫release()方法釋放資源。

由於要使用麥克風和儲存空間來進行操作,自然要喜聞樂見的許可權請求操作:

	if (ContextCompat.
	checkSelfPermission(AudioRecorderActivity.this,
	Manifest.permission.RECORD_AUDIO) 
	!= PackageManager.PERMISSION_GRANTED ||
       ContextCompat.
   	checkSelfPermission(AudioRecorderActivity.this, 
	Manifest.permission.WRITE_EXTERNAL_STORAGE) 
	!= PackageManager.PERMISSION_GRANTED) {

       ActivityCompat.requestPermissions(
       AudioRecorderActivity.this, 
       new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE,
       				Manifest.permission.RECORD_AUDIO}, 
       				1);

      } 
複製程式碼

要申請WRITE_EXTERNAL_STORAGE和RECORD_AUDIO這兩個許可權,然後根據流程,程式碼如下:

				try {
                    mRecorderFile = new File(Environment.
	                        getExternalStorageDirectory().
	                        getCanonicalFile() + 
	                        "/projectTest/sound.amr");
                    
                    if (!mRecorderFile.exists()) {
                        mRecorderFile.createNewFile();
                    }


                } catch (IOException e) {
                    e.printStackTrace();
                }
                mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
                mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
                mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                    mMediaRecorder.setOutputFile(mRecorderFile.getAbsoluteFile());
                }
                try {
                    mMediaRecorder.prepare();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                mMediaRecorder.start();
複製程式碼

首先使用File類建立檔案,然後呼叫MediaRecorder的初始化的方法,設定錄音的格式,檔案路徑等等,然後呼叫prepare方法,最後呼叫start()方法。 有時候錄音想要暫停一下,需要呼叫pause()方法,然後重新開始錄製的時候需要呼叫resume方法,停止錄音的時候和前面一樣呼叫stop方法就好。

三、使用AudioRecord錄音

AudioRecord和MediaRecorder相比更加底層一些,MediaRecorder類對於錄音的封裝相對要完備一些,AudioRecord同時也就更加靈活一些,可以直接讀取到音訊的資料,對音訊進行一些處理等等。 使用AudioRecord類來進行錄音時,可以使用AudioTrack進行流資料讀取和播放,也可以把AudioRecord的資料儲存成檔案,新增header,然後使用MediaPlayer播放。 關於使用AudioTrack進行AudioRecord的播放較為複雜,而且與實現實時語音聊天有關,因此這裡不做展開,以後再做語音聊天相關總結時會詳細說,這裡只總結關於AudioRecord進行錄音的操作。 AudioRecord的構造方法有5個引數,根據官方解釋,這5個引數的含義是這樣的:

Android聲音相關總結

關於具體的引數含義,在android中AudioRecord使用這篇中有比較詳細的講解,這裡不在贅述,直接上程式碼:

bufferSize = AudioRecord.getMinBufferSize(
            44100,
            AudioFormat.CHANNEL_IN_STEREO,
            AudioFormat.ENCODING_PCM_16BIT);
    //引數說明:
    // 音訊獲取源MIC,
    // 設定音訊取樣率,44100是目前的標準,但是某些裝置仍然支援22050,16000,11025
    // 設定音訊的錄製的聲道CHANNEL_IN_STEREO為雙聲道,CHANNEL_CONFIGURATION_MONO為單聲道,
    // 音訊資料格式:PCM 16位每個樣本。保證裝置支援。PCM 8位每個樣本。不一定能得到裝置支援,
    // 緩衝區位元組大小
    mAudioRecord = new AudioRecord(
            MediaRecorder.AudioSource.MIC,
            44100,
            AudioFormat.CHANNEL_IN_STEREO,
            AudioFormat.ENCODING_PCM_16BIT,
            bufferSize);
複製程式碼

呼叫構造方法以後直接呼叫startRecording()方法就開始錄音了,但是,同時要開一個執行緒用來儲存或者直接播放錄製的音訊資料, 線上程中讀取音訊檔案需要呼叫read等方法,官方對於方法的解釋如下:

Android聲音相關總結

一般來說是用一個while迴圈來進行資料的迴圈讀取寫入檔案中:

/**
 * 把錄製的音訊裸資料寫入到檔案中去
 * 這裡將資料寫入檔案,但是並不能播放,因為AudioRecord獲得的音訊是原始的裸音訊,
 * 如果需要播放就必須加入一些格式或者編碼的頭資訊。但是這樣的好處就是你可以對音訊的 裸資料進行處理,比如你要做一個愛說話的TOM
 * 貓在這裡就進行音訊的處理,然後重新封裝 所以說這樣得到的音訊比較容易做一些音訊的處理。
 */
private void writeDataToFile() {
    // new一個byte陣列用來存一些位元組資料,大小為緩衝區大小
    byte[] audioData = new byte[bufferSizeInBytes];
    int readSize = 0;
    FileOutputStream fos = null;
    File file = new File(AudioName);
    if (file.exists()) {
        file.delete();
    }
    try {
        fos = new FileOutputStream(file);//獲取一個檔案的輸出流
    } catch (FileNotFoundException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }

    while (isRecord) {
        readSize = audioRecord.read(audioData, 0, bufferSizeInBytes);
        Log.d(TAG, "readSize =" + readSize);
        if (AudioRecord.ERROR_INVALID_OPERATION != readSize) {
            try {
                fos.write(audioData);
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }

    try {
        fos.close();
    } catch (IOException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
}
複製程式碼

想要停止錄音呼叫stop()方法就可以了。

以上就是關於Android的系統聲音控制,播放聲音,錄製音訊檔案的一點粗淺的總結。

參考部落格:

blog.csdn.net/quincyjiang…

blog.csdn.net/ddna/articl…

http://www.jb51.net/article/105465.htm

blog.csdn.net/qq_19067845…

www.2cto.com/kf/201405/3…

blog.csdn.net/foruok/arti…

developer.android.com/reference/a…

www.runoob.com/w3cnote/and…

blog.csdn.net/fengyuzheng…

segmentfault.com/q/101000000…

blog.csdn.net/true100/art…

www.cnblogs.com/jiww/p/5619…

www.360doc.com/content/16/…

www.360doc.com/document/16…

developer.android.com/reference/a…

blog.csdn.net/hedaogelaos…

相關文章