TextureView+SurfaceTexture+OpenGL ES來播放視頻(三)

做好的Demo截圖


opengl-video
前言

講了這么多拖刃,可能有人要問了,播放視頻用個(gè)android封裝的VideoView或者用MediaPlayer+SurfaceView來進(jìn)行播放視頻不就得了嗎扎筒,干嘛還要整這么麻煩综慎。OK涣仿,為了回答這個(gè)問題勤庐,我們先看看OpenGL ES干什么的示惊,它是OpenGL三維圖形API的子集,圖形硬件的一種軟件接口愉镰,針對(duì)手機(jī)米罚、PDA和游戲主機(jī)等嵌入式設(shè)備而設(shè)計(jì)。我想如果是做游戲類開發(fā)的肯定對(duì)一些圖形庫(kù)不會(huì)陌生丈探,其實(shí)很多游戲引擎內(nèi)部都是封裝的像OpenGL录择,DirectX 3D之類的圖形庫(kù),然后開發(fā)者就可以通過這些圖形庫(kù)碗降,來開發(fā)很多好玩的東西隘竭,如游戲,動(dòng)畫等等讼渊,那么我現(xiàn)在用Opengl es來繪制視頻是不是也可以定制很多有意思的東西动看,比如開發(fā)一個(gè)左右分屏視頻播放器,然后在虛擬現(xiàn)實(shí)(VR)頭盔上來觀看2d的視頻爪幻,如果用opengl去繪制菱皆,那簡(jiǎn)直分分中搞定,因?yàn)檫@里挨稿,每一幀的視頻在opengl 看來只是一張紋理貼圖而已仇轻,那么我想把這個(gè)貼圖貼在哪里就貼在哪里∧谈剩總之篷店,用opengl可以開發(fā)出很多有意思的二維,三維的圖形應(yīng)用出來臭家。

正文

說了這么多疲陕,咱們開始吧吭产,

/***
 * 在這個(gè)類里面對(duì)視頻紋理進(jìn)行繪制工作,繼承了 {@link TextureSurfaceRenderer},
 * 并實(shí)現(xiàn)了{(lán)@link SurfaceTexture.OnFrameAvailableListener}
 */
public class VideoTextureSurfaceRenderer extends TextureSurfaceRenderer implements
        SurfaceTexture.OnFrameAvailableListener
{

    public static final String TAG = VideoTextureSurfaceRenderer.class.getSimpleName();
    /**繪制的區(qū)域尺寸*/
    private static float squareSize = 1.0f;
    private static float squareCoords[] = {
            -squareSize,  squareSize, 0.0f,   // top left
            -squareSize, -squareSize, 0.0f,   // bottom left
            squareSize, -squareSize, 0.0f,    // bottom right
            squareSize,  squareSize, 0.0f     // top right
    };
    /**繪制次序*/
    private static short drawOrder[] = {
         0, 1, 2, 
         0, 2, 3
     };
    /**
     * 用來緩存紋理坐標(biāo)鸭轮,因?yàn)榧y理都是要在后臺(tái)被繪制好臣淤,然
     * 后不斷的替換最前面顯示的紋理圖像
     */
    private FloatBuffer textureBuffer;
    /**紋理坐標(biāo)*/
    private float textureCoords[] = {
            0.0f, 1.0f, 0.0f, 1.0f,
            0.0f, 0.0f, 0.0f, 1.0f,
            1.0f, 0.0f, 0.0f, 1.0f,
            1.0f, 1.0f, 0.0f, 1.0f
    };
    /**生成的真實(shí)紋理數(shù)組*/
    private int[] textures = new int[1];
    /**著色器腳本程序的handle(句柄)*/
    private int shaderProgram;
    /**squareCoords的的頂點(diǎn)緩存*/
    private FloatBuffer vertexBuffer;
    /**繪制次序的緩存*/
    private ShortBuffer drawOrderBuffer;
    
    /**矩陣來變換紋理坐標(biāo),(具體含義下面再解釋)*/
    private float[] videoTextureTransform;
    /**當(dāng)前的視頻幀是否可以得到*/
    private boolean frameAvailable = false;
    private Context context;
    private SurfaceTexture videoTexture窃爷;    // 從視頻流捕獲幀作為Opengl ES 的Texture
    
    /**
     * @param texture 從TextureView獲取到的SurfaceTexture邑蒋,目的是為了配置EGL 的native window
    */
    public VideoTextureSurfaceRenderer(Context context, SurfaceTexture texture, int width, int height)
    {
        super(texture, width, height);  //先調(diào)用父類去做EGL初始化工作
        this.context = context;
        videoTextureTransform = new float[16];      
    }
   
    // 代碼略
}

對(duì)上面的代碼再稍稍解釋一下吧,在繪制之前按厘,我們首先選定一塊區(qū)域來讓圖像就在這塊區(qū)域來繪制医吊,則有如下定義:

private static float squareSize = 1.0f;
    private static float squareCoords[] = {
            -squareSize,  squareSize, 0.0f,   // top left
            -squareSize, -squareSize, 0.0f,   // bottom left
            squareSize, -squareSize, 0.0f,    // bottom right
            squareSize,  squareSize, 0.0f     // top right
    };

上幅圖來解釋解釋:

square

當(dāng)squareSize=1.0時(shí),square的面積就是整個(gè)手機(jī)屏幕逮京,若squareSize=0.5f則每個(gè)邊長(zhǎng)都為屏幕的一半卿堂。數(shù)組的坐標(biāo)順序?yàn)椋鹤笊?>左下->右下->右上。

然后接著定義一個(gè)紋理坐標(biāo)數(shù)組:

 private float textureCoords[] = {
            0.0f, 1.0f, 0.0f, 1.0f,  //左上
            0.0f, 0.0f, 0.0f, 1.0f,  //左下
            1.0f, 0.0f, 0.0f, 1.0f,  //右下
            1.0f, 1.0f, 0.0f, 1.0f   //右上
    };
   

接著上圖:


texCoord

如上圖懒棉,這就是2D OpenGL ES紋理坐標(biāo)草描,它由四個(gè)列向量(s, t, 0, 1)組成,其中s, t ∈[0, 1]。
不知道大家有沒有發(fā)現(xiàn)策严,繪制區(qū)域(quareCoords[])的頂點(diǎn)數(shù)組坐標(biāo)順序跟紋理數(shù)組坐標(biāo)的順序都是從 左上方開始->到右上方結(jié)束穗慕,為什么要這樣做呢? 其實(shí)目的就是為了統(tǒng)一方向妻导,因?yàn)槲覀冎朗謾C(jī)屏幕的坐標(biāo)是左上角為原點(diǎn)(0.0, 0.0)逛绵,所以為了以后不必要的轉(zhuǎn)換工作,最好將繪制的順序的都統(tǒng)一起來倔韭。

然后則看看紋理的繪制次序drawOrder[] = {0,1,2, 0, 2, 3} ; 這又是個(gè)什么次序呢术浪? 好,再來個(gè)圖看看

drawOrder

其實(shí)寿酌,opengl es 在繪制一個(gè)多邊形時(shí)胰苏,都是用一個(gè)基本的圖元即三角形拼湊出來的,比如一個(gè)矩形它可以用(0->1->2)和(0->2->3)的次序份名,通過兩個(gè)三角形給拼湊出來的碟联,當(dāng)然也可是其它的組合比如(1,3僵腺,2)(1鲤孵,3,0)等等辰如,總之得用兩個(gè)三角形拼湊成一個(gè)矩形普监,不過建議還是都按找同一鐘次序,比如都是按照順時(shí)針或都按照逆時(shí)針來拼湊。

OK凯正,接下來再貼出省略的代碼:

    /**
    * 重寫父類方法毙玻,初始化組件
    */
    @Override
    protected void initGLComponents()
    {
        setupVertexBuffer();
        setupTexture();
        loadShaders();
    }
/***
* 設(shè)置頂點(diǎn)緩存
*/
 private void setupVertexBuffer()
    {
        /** Draw Order buffer*/
        ByteBuffer orderByteBuffer = ByteBuffer.allocateDirect(drawOrder. length * 2); 
        orderByteBuffer.order(ByteOrder.nativeOrder());  //Modifies this buffer's byte order
        drawOrderBuffer = orderByteBuffer.asShortBuffer();  //創(chuàng)建此緩沖區(qū)的視圖,作為一個(gè)short緩沖區(qū).
        drawOrderBuffer.put(drawOrder);
        drawOrderBuffer.position(0); //下一個(gè)要被讀或?qū)懙脑氐乃饕壬ⅲ瑥? 開始

        // Initialize the texture holder
        ByteBuffer bb = ByteBuffer.allocateDirect(squareCoords.length * 4);
        bb.order(ByteOrder.nativeOrder());
        vertexBuffer = bb.asFloatBuffer();
        vertexBuffer.put(squareCoords);
        vertexBuffer.position(0);
    }

ByteBuffer.allocateDirect(drawOrder. length * 2)表示的是直接從系統(tǒng)中分配大小為 (drawOrder. length * 2)的內(nèi)存桑滩,2 代表一個(gè)short型占兩個(gè)字節(jié),(int占4個(gè)字節(jié))允睹。

/**接著初始化紋理*/
private void setupTexture()
    {
        ByteBuffer texturebb = ByteBuffer.allocateDirect(textureCoords.length * 4);
        texturebb.order(ByteOrder.nativeOrder());

        textureBuffer = texturebb.asFloatBuffer();
        textureBuffer.put(textureCoords);
        textureBuffer.position(0);

        // 啟用紋理
        GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
        //1表示只需要一個(gè)紋理索引
        GLES20.glGenTextures(1, textures, 0);         
       GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textures[0]);

      videoTexture = new SurfaceTexture(textures[0]);
      videoTexture.setOnFrameAvailableListener(this);
    }

關(guān)于glBindTexure(int target运准, int texture) 中的參數(shù)target,表示指定這是一張什么類型的紋理缭受,在此是GLES11Ext.GL_BLEND_EQUATION_RGB_OES胁澳,也可以是常用的2D紋理如GLES20.GL_TEXTURE_2D等;第二個(gè)參數(shù)texture米者,在程序第一次使用這個(gè)參數(shù)時(shí)韭畸,這個(gè)函數(shù)會(huì)創(chuàng)建一個(gè)新的對(duì)象,并把這個(gè)對(duì)象分配給它蔓搞,之后這個(gè)texture就成了一個(gè)活動(dòng)的紋理對(duì)象胰丁。如果texture=0 則OpenGL就停止使用紋理對(duì)象,并返回到初始的默認(rèn)紋理败明。

/**加載頂點(diǎn)與片段著色器*/
 private void loadShaders()
    {
        final String vertexShader = RawResourceReader.readTextFileFromRawResource(context, R.raw.vetext_sharder);
        final String fragmentShader = RawResourceReader.readTextFileFromRawResource(context, R.raw.fragment_sharder);

        final int vertexShaderHandle = ShaderHelper.compileShader(GLES20.GL_VERTEX_SHADER, vertexShader);
        final int fragmentShaderHandle = ShaderHelper.compileShader(GLES20.GL_FRAGMENT_SHADER, fragmentShader);
        shaderProgram = ShaderHelper.createAndLinkProgram(vertexShaderHandle, fragmentShaderHandle,
                new String[]{"texture","vPosition","vTexCoordinate","textureTransform"});
    }

關(guān)于可編程著色器在計(jì)算機(jī)圖形領(lǐng)域本身就是個(gè)很大的主題隘马,已超出了本文的范圍太防。那么就稍稍解釋下妻顶,OpenGL著色語言(OpenGL shading Language),它是一種編程語言,用于創(chuàng)建可編程的著色器蜒车。在opengl es 2.0以前使用的是一種“固定功能管線“讳嘱,而2.0以后就是使用的著這鐘可“編程的管線“,這種管線又分為頂點(diǎn)處理管線與片段處理管線(涉及到OpenGL的渲染管線的一些機(jī)制酿愧,大家自行的查吧)沥潭。
我現(xiàn)在就說說這個(gè)GLSL著色器是怎么使用的吧:有關(guān)使用GLSL創(chuàng)建著色器流程如下圖(參照OpenGL 編程指南):


shaderCreate

大家可以參照這幅圖來看看程序中shader的使用。
在本項(xiàng)目的res目錄下新建一個(gè)raw文件夾嬉挡,然后分別創(chuàng)建vertext_shader.glsl和fragment_sharder.glsl文件钝鸽,代碼如下:
vertext_sharder:

attribute vec4 vPosition;    //頂點(diǎn)著色器輸入變量由attribute來聲明
attribute vec4 vTexCoordinate;
//uniform表示一個(gè)變量的值由應(yīng)用程序在著色器執(zhí)行之前指定,
//并且在圖元處理過程中不會(huì)發(fā)生任何變化庞钢。mat4表示一個(gè)4x4矩陣
uniform mat4 textureTransform; 
varying vec2 v_TexCoordinate;   //片段著色器輸入變量用arying來聲明

void main () {
    v_TexCoordinate = (textureTransform * vTexCoordinate).xy;
    gl_Position = vPosition;
}

fragment_shader

/**使用GL_OES_EGL_image_external擴(kuò)展處理拔恰,來增強(qiáng)GLSL*/
#extension GL_OES_EGL_image_external : require
precision mediump float;
uniform samplerExternalOES texture; //定義擴(kuò)展的的紋理取樣器amplerExternalOES
varying vec2 v_TexCoordinate;

void main () {
    vec4 color = texture2D(texture, v_TexCoordinate);
    gl_FragColor = color;
}

著色器語言非常的類似C語言,也是從main函數(shù)開始執(zhí)行的基括。其中的很多語法颜懊,變量等等,還是大家自行的查查,這不是幾句能說明白的河爹。著色器的創(chuàng)建及編譯過程的代碼都在項(xiàng)目里的一個(gè)util包下的三個(gè)工具類匠璧,RawRourceReader,ShaderHelper,TextureHelper類中,我就不再貼出來了咸这,有興趣大家可以fork或clone下來看看夷恍。

ok,終于初始化完了媳维,太不容易了裁厅,冏。
好吧侨艾,繪制工作開始跑起來执虹。

   @Override
    protected boolean draw()
    {
        synchronized (this)
        {
            if (frameAvailable)
            {
                videoTexture .updateTexImage();
                videoTexture .getTransformMatrix(videoTextureTransform);
                frameAvailable = false;
            }
            else
            {
                return false;
            }

        }
        GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
        GLES20.glViewport(0, 0, width, height);
        this.drawTexture();

        return true;
    }

當(dāng)視頻的幀可得到時(shí),調(diào)用videoTexture .updateTexImage()方法唠梨,這個(gè)方法的官網(wǎng)解釋如下:
<font color=#008000 >Update the texture image to the most recent frame from the image stream. This may only be called while the OpenGL ES context that owns the texture is current on the calling thread. It will implicitly bind its texture to the GL_TEXTURE_EXTERNAL_OES texture target.</font>
大意是袋励,從的圖像流中更新紋理圖像到最近的幀中。這個(gè)函數(shù)僅僅當(dāng)擁有這個(gè)紋理的Opengl ES上下文當(dāng)前正處在繪制線程時(shí)被調(diào)用当叭。它將隱式的綁定到這個(gè)擴(kuò)展的GL_TEXTURE_EXTERNAL_OES 目標(biāo)紋理(為什么上面的片段著色代碼中要擴(kuò)展這個(gè)OES茬故,可能就是應(yīng)為這個(gè)吧)。
videoTexture .getTransformMatrix(videoTextureTransform)這又是什么意思呢蚁鳖?當(dāng)對(duì)紋理用amplerExternalOES采樣器采樣時(shí)磺芭,應(yīng)該首先使用getTransformMatrix(float[])查詢得到的矩陣來變換紋理坐標(biāo),每次調(diào)用updateTexImage()的時(shí)候醉箕,可能會(huì)導(dǎo)致變換矩陣發(fā)生變化钾腺,因此在紋理圖像更新時(shí)需要重新查詢,該矩陣將傳統(tǒng)的2D OpenGL ES紋理坐標(biāo)列向量(s,t,0,1)讥裤,其中s放棒,t∈[0,1],變換為紋理中對(duì)應(yīng)的采樣位置己英。該變換補(bǔ)償了圖像流中任何可能導(dǎo)致與傳統(tǒng)OpenGL ES紋理有差異的屬性间螟。例如,從圖像的左下角開始采樣损肛,可以通過使用查詢得到的矩陣來變換列向量(0,0,0,1)厢破,而從右上角采樣可以通過變換(1,1,0,1)來得到。

關(guān)于 GLES20.glViewport(int x, int y, int width, int height) 治拿,也是個(gè)比較重要的函數(shù)摩泪,這個(gè)函數(shù)大家網(wǎng)上查查。
ok忍啤,接下來到了真正的繪制紋理的時(shí)候了加勤。代碼如下:

 private void drawTexture() {
        // Draw texture
        GLES20.glUseProgram(shaderProgram); //繪制時(shí)使用著色程序
        int textureParamHandle = GLES20.glGetUniformLocation(shaderProgram, "texture"); //返回一個(gè)于著色器程序中變量名為"texture"相關(guān)聯(lián)的索引
        int textureCoordinateHandle = GLES20.glGetAttribLocation(shaderProgram, "vTexCoordinate");
        int positionHandle = GLES20.glGetAttribLocation(shaderProgram, "vPosition");
        int textureTransformHandle = GLES20.glGetUniformLocation(shaderProgram, "textureTransform");
        //在用VertexAttribArray前必須先激活它    
        GLES20.glEnableVertexAttribArray(positionHandle);
         //指定positionHandle的數(shù)據(jù)值可以在什么地方訪問仙辟。 vertexBuffer在內(nèi)部(NDK)是個(gè)指針,指向數(shù)組的第一組值的內(nèi)存
        GLES20.glVertexAttribPointer(positionHandle, 3, GLES20.GL_FLOAT, false, 0, vertexBuffer);               
        GLES20.glBindTexture(GLES20.GL_TEXTURE0, textures[0]);
        GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
        //指定一個(gè)當(dāng)前的textureParamHandle對(duì)象為一個(gè)全局的uniform 變量
        GLES20.glUniform1i(textureParamHandle, 0);

        GLES20.glEnableVertexAttribArray(textureCoordinateHandle);
        GLES20.glVertexAttribPointer(textureCoordinateHandle, 4, GLES20.GL_FLOAT, false, 0, textureBuffer);

        GLES20.glUniformMatrix4fv(textureTransformHandle, 1, false, videoTextureTransform, 0);
       //GLES20.GL_TRIANGLES(以無數(shù)小三角行的模式)去繪制出這個(gè)紋理圖像
        GLES20.glDrawElements(GLES20.GL_TRIANGLES, drawOrder.length, GLES20.GL_UNSIGNED_SHORT, drawOrderBuffer);
        GLES20.glDisableVertexAttribArray(positionHandle);
        GLES20.glDisableVertexAttribArray(textureCoordinateHandle);
    }

好了所有代碼就分析到這里了鳄梅,前兩篇鏈接TextureView+SurfaceTexture+OpenGL ES來播放視頻(一) 叠国, TextureView+SurfaceTexture+OpenGL ES來播放視頻(二) , 項(xiàng)目地址在here

注: 當(dāng)然了戴尸,其實(shí)也沒必要配置EGL環(huán)境這么麻煩粟焊,android的GLSurfaceView就已經(jīng)在底層就配置好了EGL環(huán)境,且自帶繪制線程
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末孙蒙,一起剝皮案震驚了整個(gè)濱河市项棠,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌挎峦,老刑警劉巖香追,帶你破解...
    沈念sama閱讀 218,755評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異坦胶,居然都是意外死亡透典,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門顿苇,熙熙樓的掌柜王于貴愁眉苦臉地迎上來峭咒,“玉大人,你說我怎么就攤上這事纪岁〈斩樱” “怎么了?”我有些...
    開封第一講書人閱讀 165,138評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵幔翰,是天一觀的道長(zhǎng)漩氨。 經(jīng)常有香客問我,道長(zhǎng)导匣,這世上最難降的妖魔是什么才菠? 我笑而不...
    開封第一講書人閱讀 58,791評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮贡定,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘可都。我一直安慰自己缓待,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評(píng)論 6 392
  • 文/花漫 我一把揭開白布渠牲。 她就那樣靜靜地躺著旋炒,像睡著了一般。 火紅的嫁衣襯著肌膚如雪签杈。 梳的紋絲不亂的頭發(fā)上瘫镇,一...
    開封第一講書人閱讀 51,631評(píng)論 1 305
  • 那天鼎兽,我揣著相機(jī)與錄音,去河邊找鬼铣除。 笑死谚咬,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的尚粘。 我是一名探鬼主播择卦,決...
    沈念sama閱讀 40,362評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼郎嫁!你這毒婦竟也來了秉继?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,264評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤泽铛,失蹤者是張志新(化名)和其女友劉穎尚辑,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體盔腔,經(jīng)...
    沈念sama閱讀 45,724評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡腌巾,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了铲觉。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片澈蝙。...
    茶點(diǎn)故事閱讀 40,040評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖撵幽,靈堂內(nèi)的尸體忽然破棺而出灯荧,到底是詐尸還是另有隱情,我是刑警寧澤盐杂,帶...
    沈念sama閱讀 35,742評(píng)論 5 346
  • 正文 年R本政府宣布逗载,位于F島的核電站,受9級(jí)特大地震影響链烈,放射性物質(zhì)發(fā)生泄漏厉斟。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評(píng)論 3 330
  • 文/蒙蒙 一强衡、第九天 我趴在偏房一處隱蔽的房頂上張望擦秽。 院中可真熱鬧,春花似錦漩勤、人聲如沸感挥。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)触幼。三九已至,卻和暖如春究飞,著一層夾襖步出監(jiān)牢的瞬間置谦,已是汗流浹背堂鲤。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評(píng)論 1 270
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留媒峡,地道東北人瘟栖。 一個(gè)月前我還...
    沈念sama閱讀 48,247評(píng)論 3 371
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像丝蹭,于是被迫代替她去往敵國(guó)和親慢宗。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評(píng)論 2 355

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