音視頻開發(fā)之旅(37) -FFmpeg + OpenGLES 邊解碼邊播放視頻(一)

目錄

  1. 基礎(chǔ)知識
  2. 使用GLSurfaceView播放邊解碼邊播放視頻
  3. 遇到的問題
  4. 資料
  5. 收獲

一怎栽、基礎(chǔ)知識

1.1. YUV和RGB

視頻是由一幅幅圖像或者說一幀幀 YUV 數(shù)據(jù)組成
表示圖片、視頻的色彩空間有幾種:YUV宿饱、RGB熏瞄、HSV等,F(xiàn)Fmpeg解碼后的視頻數(shù)據(jù)是YUV數(shù)據(jù)谬以,而OpenGL ES 渲染時(shí)要使用RGB數(shù)據(jù)强饮,為此我們需要把YUV先轉(zhuǎn)成RGB,對應(yīng)的轉(zhuǎn)換公式如下:

 rgb = mat3(
    1.0, 1.0, 1.0,
    0.0, -0.39465, 2.03211,
    1.13983, -0.5806, 0.0
    ) *yuv;

1.2 OpenGL ES基礎(chǔ)知識

我們在第二個(gè)系列中已經(jīng)對OpenGLES的基本流程和GLSL語法以及繪制各種圖形为黎、矩陣變換等進(jìn)行過學(xué)習(xí)實(shí)踐邮丰。不清楚的或者遺忘的可以回顧下行您。
OpenGL ES涉及的知識點(diǎn)和可以做的東西是非常豐富,后面還會對其有一系列更深入的學(xué)習(xí)實(shí)踐柠座。
音視頻開發(fā)之旅(七) OpenGL ES 基本概念

音視頻開發(fā)之旅(八)GLSL及Shader的渲染流程

音視頻開發(fā)之旅(九) OpenGL ES 繪制平面圖形

音視頻開發(fā)之旅(十) GLSurfaceView源碼解析&EGL環(huán)境

音視頻開發(fā)之旅(11) OpenGL ES矩陣變換與坐標(biāo)系統(tǒng)

音視頻開發(fā)之旅(12) OpenGL ES之紋理

二邑雅、使用GLSurfaceView播放解碼的YUV數(shù)據(jù)

前面幾篇我們實(shí)現(xiàn)了對視頻流的解碼生成了YUV裸流,當(dāng)時(shí)是通過YUVplayer和ffplayer在pc上進(jìn)行的驗(yàn)證妈经。這一小節(jié)淮野,我們通過Android 提供的GLSurfaceview來進(jìn)行視頻的渲染。因?yàn)镚LsurfaceView已經(jīng)有了EGL渲染線程吹泡,本篇我們先通過使用熟悉渲染流程

首先我們寫下頂點(diǎn)著色器和片源著色器骤星。
頂點(diǎn)著色器

//#version 120

attribute vec4 aPosition;
attribute vec2 aTextureCoord;

varying vec2 vTextureCoord;

void main() {
    gl_Position = aPosition;
    vTextureCoord = aTextureCoord;
}

片源著色器

//#version 120
precision mediump float;

varying vec2 vTextureCoord;

uniform sampler2D samplerY;
uniform sampler2D samplerU;
uniform sampler2D samplerV;

void main() {
    vec3 yuv;
    vec3 rgb;

    yuv.r=texture2D(samplerY, vTextureCoord).g;
    yuv.g=texture2D(samplerU, vTextureCoord).g -0.5;
    yuv.b=texture2D(samplerV, vTextureCoord).g-0.5;

    rgb = mat3(
    1.0, 1.0, 1.0,
    0.0, -0.39465, 2.03211,
    1.13983, -0.5806, 0.0
    ) *yuv;

    gl_FragColor = vec4(rgb,1.0);
}

Render代碼如下,也是比較常規(guī)的操作,又不清楚的爆哑,可以回看下OpenGL系列內(nèi)容

package android.spport.mylibrary2;

import android.content.res.Resources;
import android.opengl.GLES20;
import android.opengl.GLSurfaceView;
import android.util.Log;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;

import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;

public class MyRender implements GLSurfaceView.Renderer {
    private Resources resources;
    private int program;

    private float verCoords[] = {
//            1.0f, -1.0f,
//            -1.0f, -1.0f,
//            1.0f, 1.0f,
//            -1.0f, 1.0f
            -1f, -1f,
            1f, -1f,
            -1f, 1f,
            1f, 1f
    };
    private float textureCoords[] = {
//            1.0f, 0.0f,
//            0.0f, 0.0f,
//            1.0f, 1.0f,
//            0.0f, 1.0f
            0f,1f,
            1f, 1f,
            0f, 0f,
            1f, 0f
    };
    private final int BYTES_PER_FLOAT = 4;

    private int aPositionLocation;
    private int aTextureCoordLocation;
    private int samplerYLocation;
    private int samplerULocation;
    private int samplerVLocation;

    private FloatBuffer verCoorFB;
    private FloatBuffer textureCoorFB;

    private int[] textureIds;

    public MyRender(Resources resources) {
        this.resources = resources;
    }

    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);

        String vertexShader = ShaderHelper.loadAsset(resources, "vertex_shader.glsl");
        String fragShader = ShaderHelper.loadAsset(resources, "frag_shader.glsl");
        program = ShaderHelper.loadProgram(vertexShader, fragShader);

        aPositionLocation = GLES20.glGetAttribLocation(program, "aPosition");
        aTextureCoordLocation = GLES20.glGetAttribLocation(program, "aTextureCoord");
        samplerYLocation = GLES20.glGetUniformLocation(program, "samplerY");
        samplerULocation = GLES20.glGetUniformLocation(program, "samplerU");
        samplerVLocation = GLES20.glGetUniformLocation(program, "samplerV");

        verCoorFB = ByteBuffer.allocateDirect(verCoords.length * BYTES_PER_FLOAT)
                .order(ByteOrder.nativeOrder())
                .asFloatBuffer()
                .put(verCoords);
        verCoorFB.position(0);

        textureCoorFB = ByteBuffer.allocateDirect(textureCoords.length * BYTES_PER_FLOAT)
                .order(ByteOrder.nativeOrder())
                .asFloatBuffer()
                .put(textureCoords);
        textureCoorFB.position(0);

        //對應(yīng)Y U V 三個(gè)紋理
        textureIds = new int[3];
        GLES20.glGenTextures(3, textureIds, 0);

        for (int i = 0; i < 3; i++) {
            GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureIds[i]);

            GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_REPEAT);
            GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_REPEAT);
            GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
            GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
        }
    }

    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        GLES20.glViewport(0, 0, width, height);

    }

    @Override
    public void onDrawFrame(GL10 gl) {
        Log.i("MyRender", "onDrawFrame: width="+width+" height="+height);
        if (width > 0 && height > 0 && y != null && u != null && v != null) {

            GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
            GLES20.glUseProgram(program);

            GLES20.glEnableVertexAttribArray(aPositionLocation);
            GLES20.glVertexAttribPointer(aPositionLocation, 2, GLES20.GL_FLOAT, false, 2 * BYTES_PER_FLOAT, verCoorFB);

            GLES20.glEnableVertexAttribArray(aTextureCoordLocation);
            GLES20.glVertexAttribPointer(aTextureCoordLocation, 2, GLES20.GL_FLOAT, false, 2 * BYTES_PER_FLOAT, textureCoorFB);

            //激活紋理
            GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
            GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureIds[0]);
            GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D,
                    0,
                    GLES20.GL_LUMINANCE,
                    width,
                    height,
                    0,
                    GLES20.GL_LUMINANCE,
                    GLES20.GL_UNSIGNED_BYTE,
                    y
            );

            GLES20.glActiveTexture(GLES20.GL_TEXTURE1);
            GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureIds[1]);
            GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D,
                    0,
                    GLES20.GL_LUMINANCE,
                    width / 2,
                    height / 2,
                    0,
                    GLES20.GL_LUMINANCE,
                    GLES20.GL_UNSIGNED_BYTE,
                    u
            );

            GLES20.glActiveTexture(GLES20.GL_TEXTURE2);
            GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureIds[2]);
            GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D,
                    0,
                    GLES20.GL_LUMINANCE,
                    width/2,
                    height/2,
                    0,
                    GLES20.GL_LUMINANCE,
                    GLES20.GL_UNSIGNED_BYTE,
                    v
            );

            GLES20.glUniform1i(samplerYLocation, 0);
            GLES20.glUniform1i(samplerULocation, 1);
            GLES20.glUniform1i(samplerVLocation, 2);

            y.clear();
            y = null;
            u.clear();
            u = null;
            v.clear();
            v = null;

            GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);

            GLES20.glDisableVertexAttribArray(aPositionLocation);
            GLES20.glDisableVertexAttribArray(aTextureCoordLocation);
        }

    }

    private int width;
    private int height;
    private ByteBuffer y;
    private ByteBuffer u;
    private ByteBuffer v;

    public void setYUVRenderData(int width, int height, byte[] y, byte[] u, byte[] v) {
        this.width = width;
        this.height = height;
        this.y = ByteBuffer.wrap(y);
        this.u = ByteBuffer.wrap(u);
        this.v = ByteBuffer.wrap(v);
    }
}

視頻解碼后通過JNI洞难,CPP調(diào)用Java的回調(diào)函數(shù)把YUV數(shù)據(jù)給到j(luò)ava層的借助GlSurfaceView進(jìn)行渲染。

extern "C" {
#include "include/libavcodec/avcodec.h"
#include "include/libavformat/avformat.h"
#include "include/log.h"
#include <libswscale/swscale.h>
#include <libavutil/imgutils.h>
#include <libswresample/swresample.h>
#include <SLES/OpenSLES.h>
#include <SLES/OpenSLES_Android.h>
#include <libavutil/time.h>
}

jmethodID onCallYuvData;
jobject jcallJavaobj;

JavaVM* javaVM;
extern "C"
JNIEXPORT void JNICALL
Java_android_spport_mylibrary2_Demo_initYUVNativeMethod(JNIEnv *env, jobject thiz) {
//    jcallJavaobj = thiz;
    jcallJavaobj = env->NewGlobalRef(thiz);

    env->GetJavaVM(&javaVM);
    onCallYuvData = env->GetMethodID(env->GetObjectClass(thiz), "onCallYUVData",
                                     "(II[B[B[B)V");
}

extern "C"
JNIEXPORT jint JNICALL
Java_android_spport_mylibrary2_Demo_decodeVideo(JNIEnv *env, jobject thiz, jstring inputPath,
                                                jstring outPath) {
...
            //把數(shù)據(jù)回調(diào)給java層揭朝,通過OpenGL進(jìn)行渲染(當(dāng)然也可以在native層構(gòu)建OpenGL環(huán)境進(jìn)行實(shí)現(xiàn)队贱,這里借助了GLSurfaceView)
            if(onCallYuvData!=NULL)
            {
                jbyteArray yData = env->NewByteArray(y_size);
                jbyteArray uData = env->NewByteArray(y_size/4);
                jbyteArray vData = env->NewByteArray(y_size/4);
                
                env->SetByteArrayRegion(yData, 0, y_size,
                                        reinterpret_cast<const jbyte *>(pFrameYUV->data[0]));
                env->SetByteArrayRegion(uData, 0, y_size/4, reinterpret_cast<const jbyte *>(pFrameYUV->data[1]));
                env->SetByteArrayRegion(vData, 0, y_size/4, reinterpret_cast<const jbyte *>(pFrameYUV->data[2]));
//                env->SetByteArrayRegion(vData, 0, y_size/4, reinterpret_cast<const jbyte *>(pFrameYUV->data[1]));
//                env->SetByteArrayRegion(uData, 0, y_size/4, reinterpret_cast<const jbyte *>(pFrameYUV->data[2]));


                LOGI("native onCallYuvData widith=%d",pCodecParameters->width);

                //jcallJavaobj 在賦值時(shí)候要通過env->NewGlobalRef(thiz);設(shè)置偉全局變量,否則會出現(xiàn)野導(dǎo)致指針異常
                env->CallVoidMethod(jcallJavaobj,onCallYuvData,pCodecParameters->width,pCodecParameters->height,yData,uData,vData);

                env->DeleteLocalRef(yData);
                env->DeleteLocalRef(uData);
                env->DeleteLocalRef(vData);

                //解碼太快潭袱,來不及渲染柱嫌,導(dǎo)致前面待渲染但還沒有渲染的數(shù)據(jù)被后解碼的數(shù)據(jù)給覆蓋了。
                //由于渲染和解碼線程現(xiàn)在還沒有做分離同步以及加入解碼buffer屯换,所以此處采用延遲的方案處理解決编丘。
                av_usleep(1000 * 50);
            }
...
}

實(shí)現(xiàn)視頻的播放。

我們可以通過JNI回調(diào)彤悔,把解碼后的yuv傳給java層進(jìn)行渲染嘉抓,這本是是一個(gè)消耗,是否能夠通過CPP層直接完成渲染吶晕窑?當(dāng)然可以抑片,音頻OpenGL ES提供了Java和native的支持,我們完全可以在native層進(jìn)行渲染杨赤,只不過nativew層沒有類似GLSuerfaceView即封裝好的EGL環(huán)境蓝丙,這樣就需要我們自己創(chuàng)建GL渲染線程進(jìn)行渲染。我們后續(xù)來進(jìn)行學(xué)習(xí)實(shí)踐望拖,在native層通過解碼線程和渲染線程 使用OpenSL ES渲染播放音頻、OpenGL ES渲染視頻挫鸽。

代碼已上傳至github [https://github.com/ayyb1988/ffmpegvideodecodedemo] 歡迎交流说敏,一起學(xué)習(xí)成長。

四丢郊、遇到的問題

  1. 運(yùn)行時(shí)出現(xiàn) JNI DETECTED ERROR IN APPLICATION異常
5.963 5247-5247/? A/DEBUG: Abort message: 'JNI DETECTED ERROR IN APPLICATION: use of invalid jobject 0x7fcea23564
        from int android.spport.mylibrary2.Demo.decodeVideo(java.lang.String, java.lang.String)'
2021-03-10 06:57:35.963 5247-5247/? A/DEBUG:     x0  0000000000000000  x1  0000000000000dd6  x2  0000000000000006  x3  0000007fcea22390
2021-03-10 06:57:35.963 5247-5247/? A/DEBUG:     x4  fefeff7939517f97  x5  fefeff7939517f97  x6  fefeff7939517f97  x7  7f7f7f7f7f7fffff
2021-03-10 06:57:35.963 5247-5247/? A/DEBUG:     x8  00000000000000f0  x9  2cd4cdcb09dc01f0  x10 0000000000000001  x11 0000000000000000
2021-03-10 06:57:35.963 5247-5247/? A/DEBUG:     x12 fffffff0fffffbdf  x13 ffffffffffffffff  x14 0000000000000004  x15 ffffffffffffffff
2021-03-10 06:57:35.963 5247-5247/? A/DEBUG:     x16 0000007a3a7618c0  x17 0000007a3a73d900  x18 0000007a3c048000  x19 0000000000000dd6
2021-03-10 06:57:35.963 5247-5247/? A/DEBUG:     x20 0000000000000dd6  x21 00000000ffffffff  x22 000000799cca7cc0  x23 00000079b5130625
2021-03-10 06:57:35.963 5247-5247/? A/DEBUG:     x24 00000079b51520fd  x25 0000000000000001  x26 00000079b4fbc258  x27 0000007a3b8067c0
2021-03-10 06:57:35.963 5247-5247/? A/DEBUG:     x28 00000079b565b338  x29 0000007fcea22430
2021-03-10 06:57:35.963 5247-5247/? A/DEBUG:     sp  0000007fcea22370  lr  0000007a3a6ef0c4  pc  0000007a3a6ef0f0
2021-03-10 06:57:36.026 2647-2647/? E/ndroid.systemu: Invalid ID 0x00000000.
2021-03-10 06:57:36.047 12827-20222/? E/Hack.Hub: net.connect = I'm afraid to call its toString()
2021-03-10 06:57:36.071 5247-5247/? A/DEBUG: backtrace:
2021-03-10 06:57:36.071 5247-5247/? A/DEBUG:       #00 pc 00000000000830f0  /apex/com.android.runtime/lib64/bionic/libc.so (abort+160) (BuildId: e55e6e4c631509598633769798683023)
...
2021-03-10 06:57:36.072 5247-5247/? A/DEBUG:       #08 pc 000000000036771c  /apex/com.android.runtime/lib64/libart.so (art::(anonymous namespace)::ScopedCheck::Check(art::ScopedObjectAccess&, bool, char const*, art::(anonymous namespace)::JniValueType*)+652) (BuildId: d700c52998d7d76cb39e2001d670e654)
2021-03-10 06:57:36.072 5247-5247/? A/DEBUG:       #09 pc 000000000036c76c  /apex/com.android.runtime/lib64/libart.so (art::(anonymous namespace)::CheckJNI::CheckCallArgs(art::ScopedObjectAccess&, art::(anonymous namespace)::ScopedCheck&, _JNIEnv*, _jobject*, _jclass*, _jmethodID*, art::InvokeType, art::(anonymous namespace)::VarArgs const*)+132) (BuildId: d700c52998d7d76cb39e2001d670e654)

原因

將jobject保存在了一個(gè)全局變量里面盔沫,而沒有使用全局引用医咨,以上面的代碼為例,即本地JNI代碼里進(jìn)行了類似object = caller;的賦值架诞,這顯然是沒有用的拟淮,一旦函數(shù)返回,caller就會被GC回收銷毀谴忧,object指向的就是一個(gè)非法地址很泊,最終導(dǎo)致上面的JNI錯(cuò)誤。

解決方案:

   jcallJavaobj = thiz;
-->改為
    jcallJavaobj = env->NewGlobalRef(thiz);

2. 設(shè)置RENDERMODE_WHEN_DIRTY模式黑屏
通過查看log 數(shù)據(jù)到來后調(diào)用了requestRender沾谓,但沒有觸發(fā)onDrawFrame委造。

時(shí)序問題,GlSurfaceview被inflater之后其EGL環(huán)境的準(zhǔn)備沒有那么早均驶,通過post延遲解碼渲染

    glSurfaceView.postDelayed(new Runnable() {
            @Override
            public void run() {
                demo.initYUVNativeMethod();
                demo.decodeVideo(folderurl+"/input.mp4", externalFilesDir+"/output7.yuv");
            }
        },300);

3. 渲染出來的視頻是顛倒的

private float verCoords[] = {
            1.0f, -1.0f,//RB
           -1.0f, -1.0f,//LB
           1.0f, 1.0f,//RT
           -1.0f, 1.0f//LT

    };
    private float textureCoords[] = {
          1.0f, 0.0f,//RB
            0.0f, 0.0f,//LB
            1.0f, 1.0f,//RT
           0.0f, 1.0f//LT

    };

--》改為
private float verCoords[] = {

            -1f, -1f,//LB
            1f, -1f,//RB
            -1f, 1f,//LT
            1f, 1f//RT
    };
    private float textureCoords[] = {

            0f,1f, //LT
            1f, 1f,//RT
            0f, 0f,//LB
            1f, 0f //RB
    };

原因:OpenGL 中紋理坐標(biāo)系和頂點(diǎn)坐標(biāo)系的y軸方向都是向上的昏兆,android手機(jī)坐標(biāo)系的y軸是向下的。所以openGL->手機(jī)顯示妇穴,需要把坐標(biāo)做上下旋轉(zhuǎn)

4. 渲染出來的視頻跳幀了
通過log查看分析爬虱,發(fā)現(xiàn)是解碼太快,來不及渲染腾它,導(dǎo)致前面待渲染但還沒有渲染的數(shù)據(jù)被后解碼的數(shù)據(jù)給覆蓋了跑筝。
由于渲染和解碼線程現(xiàn)在還沒有做分離同步以及加入解碼buffer,所以此處采用延遲的方案處理解決携狭。
在Packet解碼渲染時(shí)加上50ms的延遲

 av_usleep(1000 * 50);

5. 出現(xiàn)部分區(qū)域有綠屏并且播放的某些時(shí)刻會出現(xiàn)部分區(qū)域花屏的情況
在pc上通過ffplay播放解碼后的yuv數(shù)據(jù)是正常的继蜡,而在手機(jī)上渲染出來的有問題,那邊肯定是渲染出了問題逛腿,查看render代碼發(fā)現(xiàn)稀并,YUV紋理中的V紋理的寬度和高度設(shè)置不對導(dǎo)致

GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D,
                    0,
                    GLES20.GL_LUMINANCE,
                    width,
                    height,
                    0,
                    GLES20.GL_LUMINANCE,
                    GLES20.GL_UNSIGNED_BYTE,
                    v
            );


--> 修改為

GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D,
                    0,
                    GLES20.GL_LUMINANCE,
                    width/2,
                    height/2,
                    0,
                    GLES20.GL_LUMINANCE,
                    GLES20.GL_UNSIGNED_BYTE,
                    v
            );

四、資料

  1. 音視頻學(xué)習(xí) (八) 掌握視頻基礎(chǔ)知識并使用 OpenGL ES 2.0 渲染 YUV 數(shù)據(jù)
  2. YUV <——> RGB 轉(zhuǎn)換算法
  3. Android平臺上基于OpenGl渲染yuv視頻
  4. Android萬能視頻播放器04-OpenGL ES渲染YUV紋理
  5. JNI DETECTED ERROR IN APPLICATION解決記錄

五单默、收獲

  1. 回顧YUV和RGB基礎(chǔ)知識
  2. 通過GLSurfaceView實(shí)現(xiàn)編解碼變渲染視頻數(shù)據(jù)
  3. 解決遇到的解碼和渲染不同步導(dǎo)致跳幀碘举、渲染時(shí)出現(xiàn)綠屏 花屏、渲染畫面時(shí)顛倒的等問題

感謝你的閱讀

篇外話:
原計(jì)劃時(shí)接下來幾篇是Native層渲染搁廓、音視頻同步引颈、編碼、倍速播放境蜕、rtmp推拉流等蝙场。但最近變得有些浮躁了是因?yàn)椋枰獙W(xué)習(xí)的太多了粱年,不止音視頻還有Android進(jìn)階的各種知識售滤,有個(gè)想分散精力的想法,兼顧兩者,但是精力有限完箩,有時(shí)候必須要專注到像激光一樣才能成事赐俗。
考慮到工作上最近遇到的新領(lǐng)域,業(yè)余時(shí)間和工作上的不能夠相互幫助弊知,導(dǎo)致這種心理阻逮,其實(shí)是在逃避。遇到困難秩彤,面對它叔扼,解決它。
最近工作中使用OpenGL的比較多呐舔,很多內(nèi)容也在學(xué)習(xí)實(shí)踐币励,為了工作和學(xué)習(xí)相結(jié)合達(dá)到事半功倍的效果,決定先暫停FFmpeg系列的更文珊拼,接下來我們聚焦在OpenGL ES渲染上食呻。
調(diào)整下優(yōu)先級和順序。FFmpeg我們后會有期澎现。

下一篇我們來學(xué)習(xí)實(shí)踐FBO仅胞,歡迎關(guān)注公眾號“音視頻開發(fā)之旅”,一起學(xué)習(xí)成長剑辫。

歡迎交流

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末干旧,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子妹蔽,更是在濱河造成了極大的恐慌椎眯,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件胳岂,死亡現(xiàn)場離奇詭異编整,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)乳丰,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進(jìn)店門掌测,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人产园,你說我怎么就攤上這事汞斧。” “怎么了什燕?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵粘勒,是天一觀的道長。 經(jīng)常有香客問我屎即,道長仲义,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮埃撵,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘虽另。我一直安慰自己暂刘,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布捂刺。 她就那樣靜靜地躺著谣拣,像睡著了一般。 火紅的嫁衣襯著肌膚如雪族展。 梳的紋絲不亂的頭發(fā)上森缠,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天,我揣著相機(jī)與錄音仪缸,去河邊找鬼贵涵。 笑死,一個(gè)胖子當(dāng)著我的面吹牛恰画,可吹牛的內(nèi)容都是我干的宾茂。 我是一名探鬼主播,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼拴还,長吁一口氣:“原來是場噩夢啊……” “哼跨晴!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起片林,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤端盆,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后费封,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體焕妙,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年孝偎,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了访敌。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,018評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡衣盾,死狀恐怖寺旺,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情势决,我是刑警寧澤阻塑,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站果复,受9級特大地震影響陈莽,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一走搁、第九天 我趴在偏房一處隱蔽的房頂上張望独柑。 院中可真熱鬧,春花似錦私植、人聲如沸忌栅。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽索绪。三九已至,卻和暖如春贫悄,著一層夾襖步出監(jiān)牢的瞬間瑞驱,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工窄坦, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留唤反,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓嫡丙,卻偏偏與公主長得像拴袭,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子曙博,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評論 2 345

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