Xmpp作為WebRTC信令的音視頻通話02-創(chuàng)建PeerConnection

創(chuàng)建PeerConnection的操作都封裝在PeerConnectionClient中,而且創(chuàng)建PeerConnection的操作必須是單線程的,而PeerConnection是由PeerConnectionFactory完成的把敢。

1、創(chuàng)建PeerConnectionFactory

我們前面說過PeerConnectionFactory一個用來生成PeerConnection的工廠類缔刹,同時負(fù)責(zé)初始化全局和底層交互愉粤。

/**
 * 創(chuàng)建PeerConnectionFactory的方法
 * @param context
 * @param localRender   本地視頻
 * @param renderEGLContext    
 * @param peerConnectionParameters
 */
public void createPeerConnectionFactory(
        final Context context,
        final VideoRenderer.Callbacks localRender,
        final EglBase.Context renderEGLContext,
        final PeerConnectionParameters peerConnectionParameters) {
    this.peerConnectionParameters = peerConnectionParameters;
    this.localRender = localRender;
    videoCallEnabled = peerConnectionParameters.videoCallEnabled;
    if(localRender==null)
        videoCallEnabled = false;
    // Reset variables to initial states.
    factory = null;
    preferIsac = false;
    videoCapturerStopped = false;
    isError = false;
    mediaStream = null;
    videoCapturer = null;
    renderVideo = true;
    localVideoTrack = null;
    enableAudio = true;
    localAudioTrack = null;
    this.videoWidth = peerConnectionParameters.videoWidth;
    this.videoHeight = peerConnectionParameters.videoHeight;
    this.videoFps = peerConnectionParameters.videoFps;
    statsTimer = new Timer();
        //用于保存對端的IceCandidate
    queuedRemoteCandidates = new ConcurrentHashMap<String,LinkedList<IceCandidate>>();
    if(isRTCClosed) return;

    executor.execute(new Runnable() {
        @Override
        public void run() {
            if(isRTCClosed) return;
            try {
                                //創(chuàng)建媒體約束暇务,創(chuàng)建PeerConnection時用
                createMediaConstraintsInternal();
                createPeerConnectionFactoryInternal(context, renderEGLContext);
            }
            catch (Exception e){
                reportError("Failed to create peer connection: " + e.getMessage());
                return;
            }
        }
    });
}

主要是根據(jù)PeerConnectionParameters中的參數(shù)創(chuàng)建媒體約束

/**
 * 創(chuàng)建媒體約束
 */
private void createMediaConstraintsInternal() {
    // Create peer connection constraints.
    pcConstraints = new MediaConstraints();
    //是否允許呼叫自己
    if (peerConnectionParameters.loopback) {
        pcConstraints.optional.add(
                new MediaConstraints.KeyValuePair(DTLS_SRTP_KEY_AGREEMENT_CONSTRAINT, "false"));
    } else {
        pcConstraints.optional.add(
                new MediaConstraints.KeyValuePair(DTLS_SRTP_KEY_AGREEMENT_CONSTRAINT, "true"));
    }
    pcConstraints.optional.add(new MediaConstraints.KeyValuePair(
            "RtpDataChannels", "true"));
    if (videoCallEnabled) {
        videoWidth = peerConnectionParameters.videoWidth;
        videoHeight = peerConnectionParameters.videoHeight;
        videoFps = peerConnectionParameters.videoFps;

        if (videoWidth == 0 || videoHeight == 0) {
            videoWidth = HD_VIDEO_WIDTH;
            videoHeight = HD_VIDEO_HEIGHT;
        }

        if (videoFps == 0) {
            videoFps = 30;
        }
        Logging.d(TAG, "Capturing format: " + videoWidth + "x" + videoHeight + "@" + videoFps);
    }

    audioConstraints = new MediaConstraints();

    if (peerConnectionParameters.noAudioProcessing) {
        Log.d(TAG, "Disabling audio processing");
        audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair(
                AUDIO_ECHO_CANCELLATION_CONSTRAINT, "false"));
        audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair(
                AUDIO_AUTO_GAIN_CONTROL_CONSTRAINT, "false"));
        audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair(
                AUDIO_HIGH_PASS_FILTER_CONSTRAINT, "false"));
        audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair(
                AUDIO_NOISE_SUPPRESSION_CONSTRAINT, "false"));
    }
    // Create SDP constraints.
    sdpMediaConstraints = new MediaConstraints();
    sdpMediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair(
            "OfferToReceiveAudio", "true"));
    if (videoCallEnabled || peerConnectionParameters.loopback) {
        sdpMediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair(
                "OfferToReceiveVideo", "true"));
    } else {
        sdpMediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair(
                "OfferToReceiveVideo", "false"));
    }
}

創(chuàng)建PeerConnectionFactory實例和MediaStream實例

/**
 * createPeerConnectionFactory
 * @param context
 * @param renderEGLContext
 */
private void createPeerConnectionFactoryInternal(Context context, final EglBase.Context renderEGLContext) {
    PeerConnectionFactory.initializeInternalTracer();
    if (peerConnectionParameters.tracing) {
        PeerConnectionFactory.startInternalTracingCapture(
                Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator
                        + "webrtc-trace.txt");
    }
    Log.d(TAG, "Create peer connection factory. Use video: " +
            peerConnectionParameters.videoCallEnabled);

    isError = false;

    String fieldTrials = "";
    if (peerConnectionParameters.videoFlexfecEnabled) {
        fieldTrials += VIDEO_FLEXFEC_FIELDTRIAL;
        Log.d(TAG, "Enable FlexFEC field trial.");
    }
    fieldTrials += VIDEO_VP8_INTEL_HW_ENCODER_FIELDTRIAL;
    preferredVideoCodec = VIDEO_CODEC_VP8;
    if (videoCallEnabled && peerConnectionParameters.videoCodec != null) {
        switch (peerConnectionParameters.videoCodec) {
            case VIDEO_CODEC_VP8:
                preferredVideoCodec = VIDEO_CODEC_VP8;
                break;
            case VIDEO_CODEC_VP9:
                preferredVideoCodec = VIDEO_CODEC_VP9;
                break;
            case VIDEO_CODEC_H264_BASELINE:
                preferredVideoCodec = VIDEO_CODEC_H264;
                break;
            case VIDEO_CODEC_H264_HIGH:
                // TODO(magjed): Strip High from SDP when selecting Baseline instead of using field trial.
                fieldTrials += VIDEO_H264_HIGH_PROFILE_FIELDTRIAL;
                preferredVideoCodec = VIDEO_CODEC_H264;
                break;
            default:
                preferredVideoCodec = VIDEO_CODEC_VP8;
        }
    }
    // Initialize field trials.
    Log.d(TAG, "Preferred video codec: " + preferredVideoCodec);
    PeerConnectionFactory.initializeFieldTrials(fieldTrials);

    preferIsac = false;
    if (peerConnectionParameters.audioCodec != null
            && peerConnectionParameters.audioCodec.equals(AUDIO_CODEC_ISAC)) {
        preferIsac = true;
    }
    if (!peerConnectionParameters.useOpenSLES) {
        Log.d(TAG, "Disable OpenSL ES audio even if device supports it");
        WebRtcAudioManager.setBlacklistDeviceForOpenSLESUsage(true /* enable */);
    } else {
        Log.d(TAG, "Allow OpenSL ES audio if device supports it");
        WebRtcAudioManager.setBlacklistDeviceForOpenSLESUsage(false);
    }

    if (peerConnectionParameters.disableBuiltInAEC) {
        Log.d(TAG, "Disable built-in AEC even if device supports it");
        WebRtcAudioUtils.setWebRtcBasedAcousticEchoCanceler(true);
    } else {
        Log.d(TAG, "Enable built-in AEC if device supports it");
        WebRtcAudioUtils.setWebRtcBasedAcousticEchoCanceler(false);
    }

    if (peerConnectionParameters.disableBuiltInAGC) {
        Log.d(TAG, "Disable built-in AGC even if device supports it");
        WebRtcAudioUtils.setWebRtcBasedAutomaticGainControl(true);
    } else {
        Log.d(TAG, "Enable built-in AGC if device supports it");
        WebRtcAudioUtils.setWebRtcBasedAutomaticGainControl(false);
    }

    if (peerConnectionParameters.disableBuiltInNS) {
        Log.d(TAG, "Disable built-in NS even if device supports it");
        WebRtcAudioUtils.setWebRtcBasedNoiseSuppressor(true);
    } else {
        Log.d(TAG, "Enable built-in NS if device supports it");
        WebRtcAudioUtils.setWebRtcBasedNoiseSuppressor(false);
    }

    WebRtcAudioRecord.setErrorCallback(new WebRtcAudioRecord.WebRtcAudioRecordErrorCallback() {
        @Override
        public void onWebRtcAudioRecordInitError(String errorMessage) {
            Log.e(TAG, "onWebRtcAudioRecordInitError: " + errorMessage);
            reportError(errorMessage);
        }

        @Override
        public void onWebRtcAudioRecordStartError(
                WebRtcAudioRecord.AudioRecordStartErrorCode errorCode, String errorMessage) {
            Log.e(TAG, "onWebRtcAudioRecordStartError: " + errorCode + ". " + errorMessage);
            reportError(errorMessage);
        }

        @Override
        public void onWebRtcAudioRecordError(String errorMessage) {
            Log.e(TAG, "onWebRtcAudioRecordError: " + errorMessage);
            reportError(errorMessage);
        }
    });

    WebRtcAudioTrack.setErrorCallback(new WebRtcAudioTrack.WebRtcAudioTrackErrorCallback() {
        @Override
        public void onWebRtcAudioTrackInitError(String errorMessage) {
            reportError(errorMessage);
        }

        @Override
        public void onWebRtcAudioTrackStartError(String errorMessage) {
            reportError(errorMessage);
        }

        @Override
        public void onWebRtcAudioTrackError(String errorMessage) {
            reportError(errorMessage);
        }
    });
    PeerConnectionFactory.initializeAndroidGlobals(context, peerConnectionParameters.videoCodecHwAcceleration);
    if (options != null) {
        Log.d(TAG, "Factory networkIgnoreMask option: " + options.networkIgnoreMask);
    }
    //創(chuàng)建PeerConnectionFactory
    factory = new PeerConnectionFactory(options);
    Log.d(TAG, "Peer connection factory created.");

    //創(chuàng)建本地媒體流
    mediaStream = factory.createLocalMediaStream("ARDAMS");
    if (videoCallEnabled) {
        //創(chuàng)建videoCapturer 
        videoCapturer = createVideoCapturer();
        if (videoCapturer == null) {
            Log.e(TAG,"Failed to open camera");
        }
        else {
            //創(chuàng)建視頻軌道并添加到媒體流中
            mediaStream.addTrack(createVideoTrack(videoCapturer));
        }
    }
    //創(chuàng)建音頻軌道并添加到媒體流中
    //如果videoCallEnabled == true的話泼掠,此時的mediaStream中有兩條軌道
    mediaStream.addTrack(createAudioTrack());
    if (videoCallEnabled) {
        Log.d(TAG, "EGLContext: " + renderEGLContext);
                //視頻硬件加速時需要用到renderEGLContext
        factory.setVideoHwAccelerationOptions(renderEGLContext, renderEGLContext);
    }
}

2、創(chuàng)建PeerConnection

在前面的步驟中已經(jīng)創(chuàng)建了PeerConnectionFactory的實例垦细,

/**
 * 
 * @param peerName    對方
 * @param remoteRender    遠(yuǎn)端的視頻流
 * @param iceServers    IceServer
 * @param isCallout    呼叫還是接聽
 * @param iceTransportsType
 * @param events    創(chuàng)建PeerConnection的回調(diào)
 */
public void createPeerConnection(
        final String peerName,
        final VideoRenderer.Callbacks remoteRender,
        final List<PeerConnection.IceServer> iceServers,
        final boolean isCallout,
        final PeerConnection.IceTransportsType iceTransportsType,
        final PeerConnectionEvents events) {
    Log.e(TAG,"createPeerConnection...");
    this.peerName = peerName;
    this.isCallout = isCallout;
    //this.executor = Executors.newSingleThreadScheduledExecutor();
    this.remoteRender = remoteRender;
    isError = false;
    this.events = events;
    if (peerConnectionParameters == null) {
        Log.e(TAG, "Creating peer connection without initializing factory.");
        return;
    }
    executor.execute(new Runnable() {
        @Override
        public void run() {
            createPeerConnectionInternal(mediaStream, iceServers, iceTransportsType);
            peerConnections.put(peerName,peerConnection);
        }
    });
}

創(chuàng)建PeerConnection并添加本地媒體流择镇,創(chuàng)建PeerConnection需要3個參數(shù):RTCConfiguration rtcConfig, MediaConstraints constraints, Observer observer。
constraints在創(chuàng)建factory實例時已經(jīng)創(chuàng)建完成括改,rtcConfig在方法中完成腻豌,observer需要實現(xiàn)。

/**
 * 
 * @param localmediaStream    本地視頻流
 * @param iceServers      IceServer
 * @param iceTransportsType    轉(zhuǎn)發(fā)還是P2P
 */
private void createPeerConnectionInternal(MediaStream localmediaStream, List<PeerConnection.IceServer> iceServers, PeerConnection.IceTransportsType iceTransportsType) {

    PeerConnection.RTCConfiguration rtcConfig =
            new PeerConnection.RTCConfiguration(iceServers);
    rtcConfig.tcpCandidatePolicy = PeerConnection.TcpCandidatePolicy.DISABLED;

    if(PreferenceUtil.getInstance().getString("BundlePolicy","0").equals("0")){
        rtcConfig.bundlePolicy = PeerConnection.BundlePolicy.BALANCED;
    }else if(PreferenceUtil.getInstance().getString("BundlePolicy","0").equals("1")) {
        rtcConfig.bundlePolicy = PeerConnection.BundlePolicy.MAXBUNDLE;
    }else if(PreferenceUtil.getInstance().getString("BundlePolicy","0").equals("2")){
        rtcConfig.bundlePolicy = PeerConnection.BundlePolicy.MAXCOMPAT;
    }
    if(PreferenceUtil.getInstance().getString("RtcpMuxPolicy","0").equals("0")){
        rtcConfig.rtcpMuxPolicy = PeerConnection.RtcpMuxPolicy.NEGOTIATE;
    }else if(PreferenceUtil.getInstance().getString("RtcpMuxPolicy","0").equals("1")){
        rtcConfig.rtcpMuxPolicy = PeerConnection.RtcpMuxPolicy.REQUIRE;
    }

    rtcConfig.continualGatheringPolicy = PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY;
    rtcConfig.iceTransportsType = iceTransportsType;

    rtcConfig.keyType = PeerConnection.KeyType.ECDSA;

    Log.d(TAG, "createPeerConnection begin .");

    //創(chuàng)建peerConnection實例
    peerConnection = factory.createPeerConnection(rtcConfig, pcConstraints, pcObserver);

    Log.d(TAG, "createPeerConnection finish .");
    if (dataChannelEnabled) {
        DataChannel.Init init = new DataChannel.Init();
        init.ordered = true;
        init.negotiated = false;
        init.maxRetransmits = -1;
        init.maxRetransmitTimeMs = -1;
        init.id = -1;
        init.protocol = "";
        dataChannel = peerConnection.createDataChannel("ApprtcDemo data", init);
    }

    Logging.enableTracing("logcat:", EnumSet.of(Logging.TraceLevel.TRACE_DEFAULT));
    Logging.enableLogToDebugOutput(Logging.Severity.LS_ERROR);
    mediaStream = localmediaStream;
    peerConnection.addStream(mediaStream);//將本地視頻流添加到peerConnection

    Log.d(TAG, "Peer connection created.");
}

3嘱能、實現(xiàn)PeerConnection.Observer

作為PeerConnectionClient的內(nèi)部類吝梅,創(chuàng)建PeerConnection過程中的回調(diào)

private class PCObserver implements PeerConnection.Observer{

    @Override
    public void onSignalingChange(PeerConnection.SignalingState signalingState) {
        Log.d(TAG,"SignalingState: " + signalingState);
    }

    @Override
    public void onIceConnectionChange(final PeerConnection.IceConnectionState iceConnectionState) {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                Log.d(TAG,"IceConnectionState: " + iceConnectionState);
                if (iceConnectionState == PeerConnection.IceConnectionState.CONNECTED) {
                    events.onIceConnected();
                } else if (iceConnectionState == PeerConnection.IceConnectionState.DISCONNECTED) {
                    events.onIceDisconnected();
                } else if (iceConnectionState == PeerConnection.IceConnectionState.FAILED) {
                }
            }
        });
    }

    @Override
    public void onIceConnectionReceivingChange(final boolean b) {
        Log.d(TAG,"IceConnectionReceiving changed to " + b);
    }

    @Override
    public void onIceGatheringChange(final PeerConnection.IceGatheringState iceGatheringState) {
        Log.d(TAG,"IceGatheringState: " + iceGatheringState);
    }

    @Override
    public void onIceCandidate(final IceCandidate iceCandidate) {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                Log.d(TAG,"onIceCandidate: ");
                events.onIceCandidate(iceCandidate);
            }
        });
    }

    @Override
    public void onIceCandidatesRemoved(final IceCandidate[] iceCandidates) {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                Log.d(TAG,"onIceCandidatesRemoved: ");
                events.onIceCandidatesRemoved(iceCandidates);
            }
        });
    }

    @Override
    public void onAddStream(final MediaStream mediaStream) {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                Log.d(TAG,"onAddStream " + peerConnection);
                if (peerConnection == null || isError) {
                    return;
                }
                if (mediaStream.audioTracks.size() > 1 || mediaStream.videoTracks.size() > 1) {
                    Log.d(TAG,"Weird-looking stream: " + mediaStream);
                    return;
                }
                if(mediaStream.audioTracks.size() == 1){
                    remoteAudioTrack = mediaStream.audioTracks.get(0);
                }
                if (mediaStream.videoTracks.size() == 1) {
                    remoteVideoTrack = mediaStream.videoTracks.get(0);
                    remoteVideoTrack.setEnabled(true);
                    remoteVideoTrack.addRenderer(new VideoRenderer(remoteRender));
                }
            }
        });
    }

    @Override
    public void onRemoveStream(final MediaStream mediaStream) {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                Log.d(TAG,"onRemoveStream");
                remoteAudioTrack = null;
                remoteVideoTrack = null;
            }
        });
    }

    @Override
    public void onDataChannel(final DataChannel dataChannel) {
        Log.d(TAG,"dc rev!");
    }

    @Override
    public void onRenegotiationNeeded() {
        Log.d(TAG,"onRenegotiationNeeded!");
    }

    @Override
    public void onAddTrack(final RtpReceiver rtpReceiver, final MediaStream[] mediaStreams) {
        Log.d(TAG,"onAddTrack!");
    }
}

4、總結(jié)

??至此已經(jīng)創(chuàng)建了PeerConnection對象惹骂,將音視頻數(shù)據(jù)封裝成MediaStream添加到PeerConnection中苏携。創(chuàng)建PeerConnection對象之后就可以調(diào)用PeerConnection的幾個方法了,但是還需要包裝一下对粪,以后再說右冻!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市著拭,隨后出現(xiàn)的幾起案子纱扭,更是在濱河造成了極大的恐慌,老刑警劉巖儡遮,帶你破解...
    沈念sama閱讀 218,036評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件乳蛾,死亡現(xiàn)場離奇詭異,居然都是意外死亡峦萎,警方通過查閱死者的電腦和手機屡久,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,046評論 3 395
  • 文/潘曉璐 我一進(jìn)店門忆首,熙熙樓的掌柜王于貴愁眉苦臉地迎上來爱榔,“玉大人,你說我怎么就攤上這事糙及∠暧模” “怎么了?”我有些...
    開封第一講書人閱讀 164,411評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長唇聘。 經(jīng)常有香客問我版姑,道長,這世上最難降的妖魔是什么迟郎? 我笑而不...
    開封第一講書人閱讀 58,622評論 1 293
  • 正文 為了忘掉前任剥险,我火速辦了婚禮,結(jié)果婚禮上宪肖,老公的妹妹穿的比我還像新娘表制。我一直安慰自己,他們只是感情好控乾,可當(dāng)我...
    茶點故事閱讀 67,661評論 6 392
  • 文/花漫 我一把揭開白布么介。 她就那樣靜靜地躺著,像睡著了一般蜕衡。 火紅的嫁衣襯著肌膚如雪壤短。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,521評論 1 304
  • 那天慨仿,我揣著相機與錄音久脯,去河邊找鬼。 笑死镰吆,一個胖子當(dāng)著我的面吹牛桶现,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播鼎姊,決...
    沈念sama閱讀 40,288評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼骡和,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了相寇?” 一聲冷哼從身側(cè)響起慰于,我...
    開封第一講書人閱讀 39,200評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎唤衫,沒想到半個月后婆赠,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,644評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡佳励,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,837評論 3 336
  • 正文 我和宋清朗相戀三年休里,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片赃承。...
    茶點故事閱讀 39,953評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡妙黍,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出瞧剖,到底是詐尸還是另有隱情拭嫁,我是刑警寧澤可免,帶...
    沈念sama閱讀 35,673評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站做粤,受9級特大地震影響浇借,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜怕品,卻給世界環(huán)境...
    茶點故事閱讀 41,281評論 3 329
  • 文/蒙蒙 一妇垢、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧肉康,春花似錦修己、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,889評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至纹安,卻和暖如春尤辱,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背厢岂。 一陣腳步聲響...
    開封第一講書人閱讀 33,011評論 1 269
  • 我被黑心中介騙來泰國打工光督, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人塔粒。 一個月前我還...
    沈念sama閱讀 48,119評論 3 370
  • 正文 我出身青樓结借,卻偏偏與公主長得像,于是被迫代替她去往敵國和親卒茬。 傳聞我的和親對象是個殘疾皇子船老,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,901評論 2 355

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