Android開發(fā)語音助手插件(AIDL跨進(jìn)程通信)

最近無意中發(fā)現(xiàn)有需要做語音播報插件的需求棍掐,因?yàn)榭紤]到語音助手集成會使得原應(yīng)用程序包體積變大粱侣,所以采取插件的形式去從外部下載券坞,然后安裝這樣一個插件即可完成語音合成(文字轉(zhuǎn)成語音)琅锻。主要用到的技術(shù)就是集成 百度語音合成SDK有序,AIDL跨進(jìn)程通信。 github下載地址

前戲

無圖無真相晕鹊,關(guān)鍵這并沒有什么絢麗的特效松却,哈哈 ~ 主要功能就是使用插件apk去朗讀主apk輸入的文字暴浦,兩個apk用AIDL跨進(jìn)程通信溅话。

截屏_20170311_164451.jpg

集成百度語音合成SDK

這個集成也沒啥難度,大家可以取參考上面鏈接對應(yīng)的開發(fā)文檔去看一下歌焦。簡單說下幾個重要步驟:
1.下載語音合成SDK飞几,包含Sample里面,但是是Eclipse的工程独撇,所以這里還需要自己新建一個AS工程屑墨。
2.拷貝資源文件到自己項(xiàng)目里面,再把SO文件拷貝到libs目錄下纷铣,并把jar包add到項(xiàng)目里面卵史,還需要在gradle文件里面去把libs目錄加到項(xiàng)目里面,如下圖所示:


添加資源文件及so文件

3.在AndroidManifest.xml文件配置需要的權(quán)限:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />

官網(wǎng)上是吧Appid,Key都放到AndroidManifest.xml里面搜立,我這里想在代碼里面設(shè)置,等下看下代碼就知道以躯。

編寫AIDL通信

首先在main目錄下新建aidl目錄,并且新建一個aidl類啄踊,如圖所示:aidl類很簡單忧设,就是一個接口,因?yàn)槲覀兪且邮瘴淖秩缓笞x取的所以定義了一個方法:void speak(String text);

新建aidl

這個時候可以rebuild一把這個插件module畸冲,這樣會生成該接口對應(yīng)的java文件妆档,這個文件里面雖然不用我們自己去寫憎瘸,自動生成,但是我們要知道里面有哪些主要的方法谨垃。

生成對應(yīng)java文件

好了 ,現(xiàn)在我們需要一個Service在后臺默默的等待著主app去調(diào)用它硼控,所以這個Service應(yīng)該具有可讀取傳過來文字的功能乘客,我們來看一下這個Service代碼:


import android.app.Service;
import android.content.Intent;
import android.os.Environment;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;

import com.baidu.tts.auth.AuthInfo;
import com.baidu.tts.client.SpeechError;
import com.baidu.tts.client.SpeechSynthesizer;
import com.baidu.tts.client.SpeechSynthesizerListener;
import com.baidu.tts.client.TtsMode;
import com.wzh.aidl.IVoiceInterface;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;

/**
 * author:Administrator on 2017/3/10 16:49
 * description:文件說明
 * version:版本
 */
public class VoiceService extends Service implements SpeechSynthesizerListener {
    private SpeechSynthesizer mSpeechSynthesizer;
    private static final String TAG = "AIDLService";
    private static final String SAMPLE_DIR_NAME = "baiduTTS";
    private static final String SPEECH_FEMALE_MODEL_NAME = "bd_etts_speech_female.dat";
    private static final String SPEECH_MALE_MODEL_NAME = "bd_etts_speech_male.dat";
    private static final String TEXT_MODEL_NAME = "bd_etts_text.dat";
    private static final String LICENSE_FILE_NAME = "temp_license";
    private static final String ENGLISH_SPEECH_FEMALE_MODEL_NAME = "bd_etts_speech_female_en.dat";
    private static final String ENGLISH_SPEECH_MALE_MODEL_NAME = "bd_etts_speech_male_en.dat";
    private static final String ENGLISH_TEXT_MODEL_NAME = "bd_etts_text_en.dat";
    private String mSampleDirPath;
    private static final int PRINT = 0;
    private static final int UI_CHANGE_INPUT_TEXT_SELECTION = 1;
    private static final int UI_CHANGE_SYNTHES_TEXT_SELECTION = 2;
    @Override
    public void onCreate() {
        super.onCreate();
        initialEnv();
        initialTts();
    }
    private void initialEnv() {
        if (mSampleDirPath == null) {
            String sdcardPath = Environment.getExternalStorageDirectory().toString();
            mSampleDirPath = sdcardPath + "/" + SAMPLE_DIR_NAME;
        }
        makeDir(mSampleDirPath);
        copyFromAssetsToSdcard(false, SPEECH_FEMALE_MODEL_NAME, mSampleDirPath + "/" + SPEECH_FEMALE_MODEL_NAME);
        copyFromAssetsToSdcard(false, SPEECH_MALE_MODEL_NAME, mSampleDirPath + "/" + SPEECH_MALE_MODEL_NAME);
        copyFromAssetsToSdcard(false, TEXT_MODEL_NAME, mSampleDirPath + "/" + TEXT_MODEL_NAME);
        copyFromAssetsToSdcard(false, LICENSE_FILE_NAME, mSampleDirPath + "/" + LICENSE_FILE_NAME);
        copyFromAssetsToSdcard(false, "english/" + ENGLISH_SPEECH_FEMALE_MODEL_NAME, mSampleDirPath + "/"
                + ENGLISH_SPEECH_FEMALE_MODEL_NAME);
        copyFromAssetsToSdcard(false, "english/" + ENGLISH_SPEECH_MALE_MODEL_NAME, mSampleDirPath + "/"
                + ENGLISH_SPEECH_MALE_MODEL_NAME);
        copyFromAssetsToSdcard(false, "english/" + ENGLISH_TEXT_MODEL_NAME, mSampleDirPath + "/"
                + ENGLISH_TEXT_MODEL_NAME);
    }
    private void makeDir(String dirPath) {
        File file = new File(dirPath);
        if (!file.exists()) {
            file.mkdirs();
        }
    }

    /**
     * 將sample工程需要的資源文件拷貝到SD卡中使用(授權(quán)文件為臨時授權(quán)文件,請注冊正式授權(quán))
     *
     * @param isCover 是否覆蓋已存在的目標(biāo)文件
     * @param source
     * @param dest
     */
    private void copyFromAssetsToSdcard(boolean isCover, String source, String dest) {
        File file = new File(dest);
        if (isCover || (!isCover && !file.exists())) {
            InputStream is = null;
            FileOutputStream fos = null;
            try {
                is = getResources().getAssets().open(source);
                String path = dest;
                fos = new FileOutputStream(path);
                byte[] buffer = new byte[1024];
                int size = 0;
                while ((size = is.read(buffer, 0, 1024)) >= 0) {
                    fos.write(buffer, 0, size);
                }
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if (fos != null) {
                    try {
                        fos.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                try {
                    if (is != null) {
                        is.close();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    private void initialTts() {
        this.mSpeechSynthesizer = SpeechSynthesizer.getInstance();
        this.mSpeechSynthesizer.setContext(this);
        this.mSpeechSynthesizer.setSpeechSynthesizerListener(this);
        // 文本模型文件路徑 (離線引擎使用)
        this.mSpeechSynthesizer.setParam(SpeechSynthesizer.PARAM_TTS_TEXT_MODEL_FILE, mSampleDirPath + "/"
                + TEXT_MODEL_NAME);
        // 聲學(xué)模型文件路徑 (離線引擎使用)
        this.mSpeechSynthesizer.setParam(SpeechSynthesizer.PARAM_TTS_SPEECH_MODEL_FILE, mSampleDirPath + "/"
                + SPEECH_FEMALE_MODEL_NAME);
        // 本地授權(quán)文件路徑,如未設(shè)置將使用默認(rèn)路徑.設(shè)置臨時授權(quán)文件路徑淀歇,LICENCE_FILE_NAME請?zhí)鎿Q成臨時授權(quán)文件的實(shí)際路徑易核,僅在使用臨時license文件時需要進(jìn)行設(shè)置,如果在[應(yīng)用管理]中開通了正式離線授權(quán)浪默,不需要設(shè)置該參數(shù)牡直,建議將該行代碼刪除(離線引擎)
        // 如果合成結(jié)果出現(xiàn)臨時授權(quán)文件將要到期的提示缀匕,說明使用了臨時授權(quán)文件,請刪除臨時授權(quán)即可碰逸。
        this.mSpeechSynthesizer.setParam(SpeechSynthesizer.PARAM_TTS_LICENCE_FILE, mSampleDirPath + "/"
                + LICENSE_FILE_NAME);
        // 請?zhí)鎿Q為語音開發(fā)者平臺上注冊應(yīng)用得到的App ID (離線授權(quán))
        this.mSpeechSynthesizer.setAppId("9378271"/*這里只是為了讓Demo運(yùn)行使用的APPID,請?zhí)鎿Q成自己的id乡小。*/);
        // 請?zhí)鎿Q為語音開發(fā)者平臺注冊應(yīng)用得到的apikey和secretkey (在線授權(quán))
        this.mSpeechSynthesizer.setApiKey("RC33pEHG3Kd3DOOrI2GMhHMC",
                "e8d7636d58dc96322ab704aec5999744"/*這里只是為了讓Demo正常運(yùn)行使用APIKey,請?zhí)鎿Q成自己的APIKey*/);
        // 發(fā)音人(在線引擎),可用參數(shù)為0,1,2,3饵史。满钟。。(服務(wù)器端會動態(tài)增加胳喷,各值含義參考文檔湃番,以文檔說明為準(zhǔn)。0--普通女聲吭露,1--普通男聲吠撮,2--特別男聲,3--情感男聲讲竿。泥兰。。)
        this.mSpeechSynthesizer.setParam(SpeechSynthesizer.PARAM_SPEAKER, "0");
        this.mSpeechSynthesizer.setParam(SpeechSynthesizer.PARAM_SPEED, "6");
        // 設(shè)置Mix模式的合成策略
        this.mSpeechSynthesizer.setParam(SpeechSynthesizer.PARAM_MIX_MODE, SpeechSynthesizer.MIX_MODE_DEFAULT);
        // 授權(quán)檢測接口(只是通過AuthInfo進(jìn)行檢驗(yàn)授權(quán)是否成功题禀。)
        // AuthInfo接口用于測試開發(fā)者是否成功申請了在線或者離線授權(quán)鞋诗,如果測試授權(quán)成功了,可以刪除AuthInfo部分的代碼(該接口首次驗(yàn)證時比較耗時)迈嘹,不會影響正常使用(合成使用時SDK內(nèi)部會自動驗(yàn)證授權(quán))
        AuthInfo authInfo = this.mSpeechSynthesizer.auth(TtsMode.MIX);

        if (authInfo.isSuccess()) {
        } else {
            String errorMsg = authInfo.getTtsError().getDetailMessage();
        }
        // 初始化tts
        mSpeechSynthesizer.initTts(TtsMode.MIX);
        // 加載離線英文資源(提供離線英文合成功能)
        int result =
                mSpeechSynthesizer.loadEnglishModel(mSampleDirPath + "/" + ENGLISH_TEXT_MODEL_NAME, mSampleDirPath
                        + "/" + ENGLISH_SPEECH_FEMALE_MODEL_NAME);
    }
    IVoiceInterface.Stub stub = new IVoiceInterface.Stub() {
        @Override
        public void speak(String text) throws RemoteException {
            int result = mSpeechSynthesizer.speak(text);
            if (result < 0) {
                Log.i(TAG, "failed");
            }
        }
    };

    @Override
    public IBinder onBind(Intent intent) {
        Log.i(TAG, "onBind() called");
        return stub;
    }

    @Override
    public boolean onUnbind(Intent intent) {
        Log.i(TAG, "onUnbind() called");
        this.mSpeechSynthesizer.release();
        return true;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.i(TAG, "onDestroy() called");
    }

    /*
     * @param arg0
     */
    @Override
    public void onSynthesizeStart(String utteranceId) {
    }

    /**
     * 合成數(shù)據(jù)和進(jìn)度的回調(diào)接口削彬,分多次回調(diào)
     *
     * @param utteranceId
     * @param data 合成的音頻數(shù)據(jù)。該音頻數(shù)據(jù)是采樣率為16K江锨,2字節(jié)精度吃警,單聲道的pcm數(shù)據(jù)。
     * @param progress 文本按字符劃分的進(jìn)度啄育,比如:你好啊 進(jìn)度是0-3
     */
    @Override
    public void onSynthesizeDataArrived(String utteranceId, byte[] data, int progress) {

    }

    /**
     * 合成正常結(jié)束酌心,每句合成正常結(jié)束都會回調(diào),如果過程中出錯挑豌,則回調(diào)onError安券,不再回調(diào)此接口
     *
     * @param utteranceId
     */
    @Override
    public void onSynthesizeFinish(String utteranceId) {

    }

    /**
     * 播放開始,每句播放開始都會回調(diào)
     *
     * @param utteranceId
     */
    @Override
    public void onSpeechStart(String utteranceId) {

    }

    /**
     * 播放進(jìn)度回調(diào)接口氓英,分多次回調(diào)
     *
     * @param utteranceId
     * @param progress 文本按字符劃分的進(jìn)度侯勉,比如:你好啊 進(jìn)度是0-3
     */
    @Override
    public void onSpeechProgressChanged(String utteranceId, int progress) {

    }

    /**
     * 播放正常結(jié)束,每句播放正常結(jié)束都會回調(diào)铝阐,如果過程中出錯址貌,則回調(diào)onError,不再回調(diào)此接口
     *
     * @param utteranceId
     */
    @Override
    public void onSpeechFinish(String utteranceId) {

    }

    /**
     * 當(dāng)合成或者播放過程中出錯時回調(diào)此接口
     *
     * @param utteranceId
     * @param error 包含錯誤碼和錯誤信息
     */
    @Override
    public void onError(String utteranceId, SpeechError error) {

    }
}

這里部分代碼可以參考百度語音助手文檔,我們來看主要的一個地方:

 IVoiceInterface.Stub stub = new IVoiceInterface.Stub() {
        @Override
        public void speak(String text) throws RemoteException {
            int result = mSpeechSynthesizer.speak(text);
            if (result < 0) {
                Log.i(TAG, "failed");
            }
        }
    };
 @Override
 public IBinder onBind(Intent intent) {
        Log.i(TAG, "onBind() called");
        return stub;
 }

這里聲明的stub對象我們可以看到,在這個方法里面是實(shí)現(xiàn)了讀取文字的功能练对,因?yàn)樗羌蒊Binder 的遍蟋,所以可以直接返回。
好了螟凭,到現(xiàn)在基本這個插件已經(jīng)具備語音合成的功能了虚青,但是我不想要圖標(biāo)顯示在桌面,所以這里去掉圖標(biāo)螺男,并且注冊Service,這里的action我們要記住棒厘,因?yàn)榭蛻舳艘残枰玫剑?/p>

<application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:supportsRtl="true"
    android:theme="@style/AppTheme">

    <service android:name=".VoiceService" android:process=":remote">
        <intent-filter>
            <action android:name="android.intent.action.VoiceService" />
            <category android:name="android.intent.category.DEFAULT" />
        </intent-filter>
    </service>
</application>

到這里插件基本可以使用了,現(xiàn)在來新建一個客戶端程序下隧。
拷貝aidl包及aidl文件奢人,要保持和插件的aidl包名及文件一致。
新建一個布局一個按鈕一個EditText汪拥,我們要實(shí)現(xiàn)的就是輸入文字达传,插件去郎讀篙耗。

import android.app.Service;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.os.RemoteException;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;

import com.wzh.aidl.IVoiceInterface;

public class MainActivity extends AppCompatActivity {
    private EditText input ;
    private Button button ;
    /**
     * 創(chuàng)建遠(yuǎn)程服務(wù)
     */
    private IVoiceInterface iVoiceInterface;
    private ServiceConnection conn = new ServiceConnection() {

        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            iVoiceInterface = IVoiceInterface.Stub.asInterface(service);
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            iVoiceInterface = null ;
        }
    };
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        /**
         * 綁定迫筑、啟動所謂的服務(wù)端 服務(wù)
         */
        final Intent intent = new Intent();
        intent.setAction("android.intent.action.VoiceService");
        intent.setPackage("com.wzh.baiduvoice");
        bindService(intent,conn, Service.BIND_AUTO_CREATE);

        input = (EditText) findViewById(R.id.input);
        button = (Button) findViewById(R.id.speak);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                String text = input.getText().toString();
                if (iVoiceInterface!=null){
                    try {
                        iVoiceInterface.speak(text);
                    } catch (RemoteException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        unbindService(conn);
    }
}

我們在創(chuàng)建Activity的時候去綁定插件的那個Service,新建一個ServiceConnection并獲取iVoiceInterface對象宗弯,當(dāng)點(diǎn)擊朗讀按鈕的時候調(diào)用插件Service的speak( )方法脯燃。
這里大家可以發(fā)現(xiàn)是顯示啟動Service的,因?yàn)锳ndroid 5.0之后google出于安全的角度禁止了隱式聲明Intent來啟動Service.也禁止使用Intent filter蒙保,隱式啟動會報錯辕棚,所以采用這種啟動方式。

如此一來我們輸入文字邓厕,就可以聽到插件給我們朗讀的聲音逝嚎!

其實(shí)這里我遇到一個問題,就是這個插件apk可能會被有的系統(tǒng)自帶的手機(jī)管家禁止關(guān)聯(lián)啟動详恼,我用的vivo x6splus測試就是被禁止了补君,目前還沒找到比較好的解決方案,希望有知道的同學(xué)告之~

這里其實(shí)只是AIDL最簡單的使用方式了昧互,如果有問題請指出挽铁,謝謝~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市敞掘,隨后出現(xiàn)的幾起案子叽掘,更是在濱河造成了極大的恐慌,老刑警劉巖玖雁,帶你破解...
    沈念sama閱讀 219,270評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件更扁,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)浓镜,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評論 3 395
  • 文/潘曉璐 我一進(jìn)店門赊堪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人竖哩,你說我怎么就攤上這事哭廉。” “怎么了相叁?”我有些...
    開封第一講書人閱讀 165,630評論 0 356
  • 文/不壞的土叔 我叫張陵遵绰,是天一觀的道長。 經(jīng)常有香客問我增淹,道長椿访,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,906評論 1 295
  • 正文 為了忘掉前任虑润,我火速辦了婚禮成玫,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘拳喻。我一直安慰自己哭当,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,928評論 6 392
  • 文/花漫 我一把揭開白布冗澈。 她就那樣靜靜地躺著钦勘,像睡著了一般。 火紅的嫁衣襯著肌膚如雪亚亲。 梳的紋絲不亂的頭發(fā)上彻采,一...
    開封第一講書人閱讀 51,718評論 1 305
  • 那天,我揣著相機(jī)與錄音捌归,去河邊找鬼肛响。 笑死,一個胖子當(dāng)著我的面吹牛惜索,可吹牛的內(nèi)容都是我干的特笋。 我是一名探鬼主播,決...
    沈念sama閱讀 40,442評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼门扇,長吁一口氣:“原來是場噩夢啊……” “哼雹有!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起臼寄,我...
    開封第一講書人閱讀 39,345評論 0 276
  • 序言:老撾萬榮一對情侶失蹤霸奕,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后吉拳,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體质帅,經(jīng)...
    沈念sama閱讀 45,802評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,984評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了煤惩。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片嫉嘀。...
    茶點(diǎn)故事閱讀 40,117評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖魄揉,靈堂內(nèi)的尸體忽然破棺而出剪侮,到底是詐尸還是另有隱情,我是刑警寧澤洛退,帶...
    沈念sama閱讀 35,810評論 5 346
  • 正文 年R本政府宣布瓣俯,位于F島的核電站,受9級特大地震影響兵怯,放射性物質(zhì)發(fā)生泄漏彩匕。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,462評論 3 331
  • 文/蒙蒙 一媒区、第九天 我趴在偏房一處隱蔽的房頂上張望驼仪。 院中可真熱鬧,春花似錦袜漩、人聲如沸绪爸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,011評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽毡泻。三九已至胜茧,卻和暖如春粘优,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背呻顽。 一陣腳步聲響...
    開封第一講書人閱讀 33,139評論 1 272
  • 我被黑心中介騙來泰國打工雹顺, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人廊遍。 一個月前我還...
    沈念sama閱讀 48,377評論 3 373
  • 正文 我出身青樓嬉愧,卻偏偏與公主長得像,于是被迫代替她去往敵國和親喉前。 傳聞我的和親對象是個殘疾皇子没酣,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,060評論 2 355

推薦閱讀更多精彩內(nèi)容