Android屏幕镜像五:局域网镜像实现

前面实现了本地录制屏幕编码保存本机播放和录制手机音频数据保存本机播放。
把录制的屏幕数据和声音数据通过局域网传输到另一台Android设备上实时解码显示就可以实现一个简单的局域网镜像功能。
镜像的实现是由一个发送端一个接收端共同完成的,接下来用phone代指发送端,tv代指接收端。

一、tv端启动镜像服务

由于这里仅是一个简单的demo,没有实现局域网内设备发现,有兴趣的同学可以自行通过mDnsUPnP实现局域网内设备发现。
这里在tv端生成一个带有镜像服务信息的二维码,phone通过扫码获取tv镜像服务信息。

1、生成局域网内的http服务

借助nanohttpd可以生成一个可在局域网内访问的http服务,将这个服务作为镜像服务的主服务端口,通过这个主服务,phone可获取tv端详细的镜像服务信息。

集成nanohttpd
dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    ...
    implementation 'org.nanohttpd:nanohttpd:2.2.0'
}
处理http消息
public class TVServer extends NanoHTTPD {
    private static final String TAG = "LelinkHTTPD";
    private Context mContext;

    TVServer(Context context, int port) {
        super(port);
        this.mContext = context;
    }

    @Override
    public Response serve(IHTTPSession session) {
        Logger.i(TAG, "url:" + session.getUri());
        Method method = session.getMethod();
        if (method.equals(Method.GET)) {
            // get请求
            return handleGetRequest(session);
        } else if (method.equals(Method.POST)) {
            // post请求
            return handlePostRequest(session);
        }
        return super.serve(session);
    }

    private Response handleGetRequest(IHTTPSession session) {
        String uri = session.getUri();
        if (uri.endsWith("/startMirror")) {
            Map<String, String> params = session.getParms();
            String decoder = params.get("decoder");
            if (!TextUtils.isEmpty(decoder)) {
                Config.getInstance().setDecoderType(decoder);
            }
            MirrorRender.getInstance().startAndroidReceiver();
            int videoPort = MirrorRender.getInstance().getAndroidVideoPort();
            int audioPort = MirrorRender.getInstance().getAndroidAudioPort();
            try {
                JSONObject jsonObject = new JSONObject();
                jsonObject.put("videoPort", videoPort);
                jsonObject.put("audioPort", audioPort);
                Logger.i(TAG,"startMirror " + jsonObject.toString());
                ByteArrayInputStream stream = new ByteArrayInputStream(jsonObject.toString().getBytes("UTF-8"));
                Response response = newChunkedResponse(Response.Status.OK, NanoHTTPD.MIME_PLAINTEXT, stream);
                response.addHeader("Access-Control-Allow-Origin", "*");
                return response;
            } catch (Exception e) {
                Logger.w(TAG, e);
            }
        } else if (uri.endsWith("/stopMirror")) {
            Logger.i(TAG, "handleGetRequest stopMirror");
            Activity mirrorActivity = Config.getInstance().getMirrorActivity();
            if (mirrorActivity != null) {
                mirrorActivity.finish();
            }
            MirrorRender.getInstance().stopRender();
            return newFixedLengthResponse(Response.Status.OK, NanoHTTPD.MIME_HTML, Response.Status.OK.getDescription());
        }
        return newFixedLengthResponse(Response.Status.BAD_REQUEST, NanoHTTPD.MIME_HTML, Response.Status.BAD_REQUEST.getDescription());
    }

    private Response handlePostRequest(IHTTPSession session) {
        Logger.i(TAG, "handlePostRequest");
        return newFixedLengthResponse(Response.Status.BAD_REQUEST, NanoHTTPD.MIME_HTML, Response.Status.BAD_REQUEST.getDescription());
    }

这里处理了两条请求,startMirror stopMirrorphone可通过
http://ip:port/startMirror http://ip:port/stopMirror对tv端请求。
startMirror中,通过MirrorRender获取到了用于接收镜像数据流的两个服务端口,phone可通过获取到的这两个端口分别传输视频流数据和音频流数据到tv端。

2、视频流服务

由于视频流不能丢失,所以这里选择tcp的方式进行传输,优先保证其稳定性。

public class AndroidVideoReceiver {
    private final static String TAG = "AndroidVideoReceiver";
    private MiniServerSocket mSocket;
    private OnReceiverListener mServerListener;
    private boolean isStop = false;
    private long mDataCount = 0;
    private long mFrameCount = 0;

    public void startReceiver() {
        try {
            mSocket = new MiniServerSocket(0);
        } catch (Exception e) {
            Logger.w(TAG, e);
            stopReceiver();
            return;
        }
        Logger.i(TAG, "startMirror ");
        if (mServerListener != null) {
            mServerListener.onReceiveStarted(OnReceiverListener.ANDROID_VIDEO);
        }
        receiveVideo();
    }

    private Thread mVideoThread;

    private void receiveVideo() {
        mVideoThread = new Thread(new Runnable() {
            @Override
            public void run() {
                receive();
            }
        });
        mVideoThread.start();
    }

    public void receive() {
        try {
            isStop = false;
            while (!isStop) {
                Socket socket = mSocket.accept();
                //Logger.i(TAG, "accept **********");
                InputStream input = socket.getInputStream();
                byte[] buf = new byte[1024];
                int len = 0;
                byte[] head = null;
                int headIndex = 0;
                int dataSize = 0;
                byte[] frame = null;
                int frameIndex = 0;
                while ((len = input.read(buf)) != -1) {
                    //Logger.i(TAG, "receive new pact data " + len);
                    mDataCount += len;
                    int readIndex = 0;
                    int bufLen = len;
                    while (readIndex < len) {
                        if (dataSize == 0) {
                            //Logger.i(TAG, "receive data new ############# headIndex:" + headIndex + " readIndex:" + readIndex);
                            if (headIndex == 0) {
                                head = new byte[4];
                            }
                            int readHead = Math.min(head.length - headIndex, len - readIndex);
                            System.arraycopy(buf, readIndex, head, headIndex, readHead);
                            headIndex += readHead;
                            readIndex += readHead;
                            if (headIndex == head.length) {
                                dataSize = CodecUtils.bytesToInt(head);
                                //Logger.i(TAG, "receive data new frame size:" + dataSize);
                                frame = new byte[dataSize];
                                headIndex = 0;
                            } else {
                                //Logger.w(TAG, "receive head ****************** headIndex:" + headIndex + "  readIndex:" + readIndex);
                                break;
                            }
                        }
                        int bufLeft = bufLen - readIndex;
                        int frameLeft = frame.length - frameIndex;
                        //Logger.i(TAG, "receive data bufLeft:" + bufLeft + "  frameLeft:" + frameLeft);
                        if (bufLeft < frameLeft) {
                            System.arraycopy(buf, readIndex, frame, frameIndex, bufLeft);
                            frameIndex += bufLeft;
                            readIndex += bufLeft;
                        } else {
                            System.arraycopy(buf, readIndex, frame, frameIndex, frameLeft);
                            frameIndex += frameLeft;
                            readIndex += frameLeft;
                            if (mServerListener != null) {
                                mServerListener.onReceiveFrame(OnReceiverListener.ANDROID_VIDEO, resolveFrame(frame));
                            }
                            mFrameCount++;
                            //one frame read complete
                            dataSize = 0;
                            frameIndex = 0;
                        }
                        //Logger.i(TAG, "receive data readIndex:" + readIndex + "  frameIndex:" + frameIndex);
                    }
                }
            }
        } catch (Exception e) {
            Logger.w(TAG, e);
            stopReceiver();
        }
    }

    private Frame resolveFrame(byte[] data) {
        int index = 0;
        byte[] headSizeBytes = new byte[4];
        System.arraycopy(data, index, headSizeBytes, 0, headSizeBytes.length);
        index += headSizeBytes.length;
        int headSize = CodecUtils.bytesToInt(headSizeBytes);
        byte[] head = new byte[headSize];
        System.arraycopy(data, index, head, 0, head.length);
        index += head.length;
        String headContent = new String(head);
        //Logger.i(TAG, "headContent " + headContent);
        Map<String, String> parameter = resolveHead(headContent);
        long pts = Long.parseLong(parameter.get("pts"));
        byte[] frameBytes = new byte[data.length - headSizeBytes.length - head.length];
        System.arraycopy(data, index, frameBytes, 0, frameBytes.length);
        Frame frame = new Frame();
        frame.buf = frameBytes;
        frame.pts = pts;
        return frame;
    }

    private Map<String, String> resolveHead(String head) {
        Map<String, String> map = new HashMap<>();
        String[] arr = head.split("&");
        for (String str : arr) {
            String[] arr1 = str.split("=");
            map.put(arr1[0], arr1[1]);
        }
        return map;
    }

    public void setOnReceiverListener(OnReceiverListener listener) {
        mServerListener = listener;
    }

    public int getReceiverPort() {
        if (mSocket != null) {
            return mSocket.getLocalPort();
        }
        return -1;
    }

    public long getDataCount() {
        return mDataCount;
    }

    public long getFrameCount() {
        return mFrameCount;
    }

    public void stopReceiver() {
        if (isStop) {
            return;
        }
        isStop = true;
        if (mSocket != null) {
            try {
                mSocket.close();
            } catch (Exception e) {
                Logger.w(TAG, e);
            }
            mSocket = null;
        }
        if (mServerListener != null) {
            mServerListener.onReceiveStopped(OnReceiverListener.ANDROID_VIDEO);
        }
    }

}

3、音频流服务

音频数据不怕丢失,所以这里选择udp进行传输。

public class AndroidAudioReceiver {
    private final static String TAG = "AndroidAudioReceiver";

    private MiniDatagramSocket mSocket;
    private static final int DATA_LEN = 4096;
    private byte[] inBuff = new byte[DATA_LEN];
    private DatagramPacket inPacket = new DatagramPacket(inBuff, inBuff.length);
    private OnReceiverListener mListener;
    private Thread mAudioThread;
    private boolean isStop = false;
    private Decoder mOpusDecoder;

    public void startReceiver() {
        try {
            mSocket = new MiniDatagramSocket(0);
        } catch (Exception e) {
            Logger.w(TAG, e);
            stopReceiver();
            return;
        }
        Logger.i(TAG, "startReceiver");
        if (mListener != null) {
            mListener.onReceiveStarted(OnReceiverListener.ANDROID_AUDIO);
        }
        receiveAudio();
    }

    private void receiveAudio() {
        mAudioThread = new Thread(new Runnable() {
            @Override
            public void run() {
                receive();
            }
        });
        mAudioThread.start();
    }

    public void receive() {
        try {
            while (!isStop) {
                try {
                    mSocket.receive(inPacket);
                    //Logger.e(TAG, "receive length:" + inPacket.getLength());
                    byte[] data = new byte[inPacket.getLength()];
                    System.arraycopy(inPacket.getData(), 0, data, 0, data.length);
                    Frame frame = new Frame();
                    frame.buf = data;
                    //Logger.e(TAG, "receive opus:" + Arrays.toString(data));
                    //Logger.e(TAG, "receive pcm:" + Arrays.toString(frame.shortBuf));
                    if (mListener != null) {
                        mListener.onReceiveFrame(OnReceiverListener.ANDROID_VIDEO, frame);
                    }
                } catch (Exception e) {
                    Logger.w(TAG, "receive", e);
                }
            }
        } catch (Exception e) {
            Logger.w(TAG, e);
            stopReceiver();
        }
    }

    public int getPort() {
        if (mSocket != null) {
            return mSocket.getLocalPort();
        }
        return -1;
    }

    public void setOnReceiverListener(OnReceiverListener listener) {
        mListener = listener;
    }

    public void stopReceiver() {
        if (isStop) {
            return;
        }
        isStop = true;
        if (mSocket != null) {
            try {
                mSocket.close();
            } catch (Exception e) {
                Logger.w(TAG, e);
            }
            mSocket = null;
        }
        if (mListener != null) {
            mListener.onReceiveStopped(OnReceiverListener.ANDROID_AUDIO);
        }
    }
}

4、生成二维码

    private void updateServerInfo() {
        if (mIPTxt != null) {
            mIPTxt.setText(DeviceUtils.getIP(getActivity()) + ":" + MirrorRender.getInstance().getTVPort());
        }
        mServerInfo = "ip=" + DeviceUtils.getIP(getActivity())
                + "&port=" + MirrorRender.getInstance().getTVPort();
        mQRView.setImageBitmap(Utils.createQRCode(mServerInfo, 200, 0));
    }

二维码中含有tv端的ip和主服务端口,phone通过这个ip和主服务端口可以发起镜像。

二、phone扫码连接

通过摄像头扫码显示,这里偷个懒使用一个比较好的第三方扫码库。

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    ...
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'com.google.zxing:core:3.3.2'
    implementation 'cn.bingoogolapple:bga-qrcode-zxing:1.3.7'
    implementation("com.squareup.okhttp3:okhttp:4.6.0")
}
扫码解析
public class ScanFragment extends BaseFragment {
    private final static String TAG = "ScanActivity";

    private QRCodeView.Delegate mQRDelegate = new QRCodeView.Delegate() {
        @Override
        public void onScanQRCodeSuccess(String result) {
            vibrate();
            ((MainActivity) getActivity()).notifyScanResult(result);
            getFragmentManager().popBackStack();
        }

        @Override
        public void onCameraAmbientBrightnessChanged(boolean isDark) {

        }

        @Override
        public void onScanQRCodeOpenCameraError() {
            Logger.i(TAG, "onScanQRCodeOpenCameraError");
        }
    };

    private ZXingView mQRCodeView;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        return View.inflate(getActivity(), R.layout.f_scan, null);
    }

    @Override
    public void onViewCreated(View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);

        mQRCodeView = view.findViewById(R.id.zxingview);
        mQRCodeView.startCamera();
        mQRCodeView.startSpot();
        mQRCodeView.setDelegate(mQRDelegate);
    }

    private void vibrate() {
        Vibrator vibrator = (Vibrator) getActivity().getSystemService(VIBRATOR_SERVICE);
        vibrator.vibrate(200);
    }

    @Override
    public void onStop() {
        super.onStop();
        mQRCodeView.stopCamera();
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        mQRCodeView.onDestroy();
    }
}
解析ip和镜像服务主端口
    public void updateSinkInfo(String info) {
        Logger.i(TAG, "updateSinkInfo " + info);
        Map<String, String> map = new HashMap<>();
        String[] arr = info.split("&");
        for (String str : arr) {
            String[] res = str.split("=");
            if (res.length > 1) {
                map.put(res[0], res[1]);
            }
        }
        String ip = map.get(KEY_IP);
        String port = map.get(KEY_PORT);
        if (mPortEdit != null) {
            mIPEdit.setText(ip);
            mPortEdit.setText(port);
        } else {
            Logger.i(TAG, "invalid receive");
        }
    }

三、phone发送视频流数据

开始镜像
    private void startMirror() {
        String ip = getEditString(mIPEdit);
        int port = getEditInt(mPortEdit);
        if (TextUtils.isEmpty(ip) || port <= 0) {
            Toast.makeText(getActivity(), "请输入正确的IP和端口", Toast.LENGTH_SHORT).show();
            return;
        }
        Config.getInstance().setTVIP(ip);
        Config.getInstance().setTVPort(port);
        OkHttpClient client = new OkHttpClient();
        Request request = new Request.Builder()
                .url("http://" + ip + ":" + port + "/startMirror" + "?decoder=" + Config.getInstance().getEncoderType())
                .build();
        Logger.i(TAG, "http://" + ip + ":" + port + "/startMirror" + "?decoder=" + Config.getInstance().getEncoderType());
        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                mHandler.obtainMessage(WHAT_TOAST, "请求失败").sendToTarget();
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                try {
                    String result = response.body().string();
                    JSONObject json = new JSONObject(result);
                    int videoPort = json.optInt("videoPort");
                    int audioPort = json.optInt("audioPort");
                    if (videoPort <= 0 || audioPort <= 0) {
                        mHandler.obtainMessage(WHAT_TOAST, "TV端口异常").sendToTarget();
                        return;
                    }
                    Config.getInstance().setVideoPort(videoPort);
                    Config.getInstance().setAudioPort(audioPort);
                    ((MainActivity) getActivity()).requestScreenCapture();
                } catch (Exception e) {
                    Logger.w(TAG, e);
                    mHandler.obtainMessage(WHAT_TOAST, "数据解析失败").sendToTarget();
                }
            }
        });
    }

在屏幕数据回调中,传输视频流数据到tv端

        @Override
        public void onCaptureVideoCallback(byte[] buf, long pts) {
//            Logger.i(TAG, "onCaptureVideoCallback " + pts);
            long start = System.currentTimeMillis();
            byte[] newBuf = new byte[buf.length];
            Frame videoFrame = new Frame();
            System.arraycopy(buf, 0, newBuf, 0, buf.length);
            videoFrame.buf = newBuf;
            videoFrame.pts = pts;
            mVideoSender.writeData(videoFrame);
            //Logger.i(TAG, "onCaptureVideoCallback cost " + (System.currentTimeMillis() - start));
        }
public class VideoSender {
    private final static String TAG = "VideoSender";
    private MiniSocket mSocket;
    private OutputStream mWriteStream;
    private ConcurrentLinkedQueue<Frame> mFrameQueue = new ConcurrentLinkedQueue<>();
    private Thread mWriteThread = null;
    private OnSendListener mListener;

    public void connect(String ip, int port) {
        Logger.i(TAG, "connect " + port);
        try {
            mSocket = new MiniSocket(ip, port);
            mSocket.setTcpNoDelay(true);
            mSocket.setKeepAlive(true);
            mWriteStream = mSocket.getOutputStream();
            startWriteThread();
        } catch (Exception e) {
            Logger.w(TAG, e);
            if (mListener != null) {
                mListener.onError();
            }
        }
    }

    private void startWriteThread() {
        mWriteThread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    if (mWriteStream == null) {
                        return;
                    }
                    if (mFrameQueue.isEmpty()) {
                        continue;
                    }
                    Frame frame = mFrameQueue.poll();
                    if (frame == null) {
                        continue;
                    }
                    long start = System.currentTimeMillis();
                    byte[] head = ("pts=" + frame.pts).getBytes();// 多个参数之间 & 分割
                    byte[] headCount = CodecUtils.intToBytes(head.length);// 多个参数之间 & 分割
                    byte[] totalCount = CodecUtils.intToBytes(headCount.length + head.length + frame.buf.length);

                    byte[] bytes = new byte[totalCount.length + headCount.length + head.length + frame.buf.length];

                    //Logger.i(TAG, "writeData  frameLen " + frame.buf.length + " headLen " + head.length);
                    int index = 0;
                    System.arraycopy(totalCount, 0, bytes, index, totalCount.length);
                    index += totalCount.length;
                    System.arraycopy(headCount, 0, bytes, index, headCount.length);
                    index += headCount.length;
                    System.arraycopy(head, 0, bytes, index, head.length);
                    index += head.length;
                    System.arraycopy(frame.buf, 0, bytes, index, frame.buf.length);
                    try {
                        mWriteStream.write(bytes);
                    } catch (Exception e) {
                        Logger.w(TAG, e);
                        mWriteStream = null;
                        if (mListener != null) {
                            mListener.onError();
                        }
                        break;
                    }
                    if (mFrameQueue.size() > 5) {
                        MirrorApplication.getApplication().changeBitrate(256 * 1024);
                    } else {
                        MirrorApplication.getApplication().changeBitrate(2 * 1024 * 1024);
                    }
                    long cost = (System.currentTimeMillis() - start);
                    if (cost > 16) {
                        Logger.i(TAG, "send video frame cost:" + cost + " len:" + bytes.length + " left frames:" + mFrameQueue.size());
                    }
                }
                disconnect();
            }
        });
        mWriteThread.start();
    }

    public void writeData(Frame frame) {
        //Logger.i(TAG,"writeData");
        mFrameQueue.add(frame);
    }

    public void disconnect() {
        try {
            if (mWriteStream != null) {
                mWriteStream.close();
                mWriteStream = null;
            }
        } catch (Exception e) {
            Logger.w(TAG, e);
        }
        try {
            if (mSocket != null) {
                mSocket.close();
                mSocket = null;
            }
        } catch (Exception e) {
            Logger.w(TAG, e);
        }
    }

    public void release() {
        mFrameQueue.clear();
    }

    public void setOnSendListener(OnSendListener listener) {
        mListener = listener;
    }
}

四、phone发送端音频流数据

与发送端视频流不同的是,音频数据发送使用udp

    private AudioCapture.OnAudioCaptureCallback mAudioCallback = new AudioCapture.OnAudioCaptureCallback() {
        @Override
        public void onCaptureAudioCallback(short[] buf) {
            Logger.i(TAG, "opus---- onCaptureAudioCallback: " + Arrays.toString(buf));
            mAudioSender.writeData(buf);
        }
    };

AudioSender

public class AudioSender {
    private final static String TAG = "PCMSender";
    private MiniDatagramSocket mSocket;
    private boolean disconnect = true;
    private String mTargetIP;
    private int mTargetPort;
    private static final int DATA_LEN = 4096;
    private OnSendListener mListener;

    public void connect(String ip, int port) {
        mTargetIP = ip;
        mTargetPort = port;
        try {
            Logger.i(TAG, "connect " + ip + "/" + port);
            mSocket = new MiniDatagramSocket(0);
            disconnect = false;
            Logger.i(TAG, "connect success " + ip + "/" + port);
        } catch (Exception e) {
            Logger.w(TAG, "connect", e);
            disconnect = true;
            if (mListener != null) {
                mListener.onError();
            }
        }
    }

    public void writeData(Frame frame) {
        if (disconnect) {
            Logger.i(TAG,"writeData ignore");
            return;
        }
        Logger.i(TAG,"writeData " + Arrays.toString(frame.buf));
        int readOffset = 0;
        while (readOffset < frame.buf.length) {
            int left = Math.min(DATA_LEN, frame.buf.length - readOffset);
            byte[] buf = new byte[left];
            System.arraycopy(frame.buf, readOffset, buf, 0, buf.length);
            try {
                InetAddress address = InetAddress.getByName(mTargetIP);
                DatagramPacket packet = new DatagramPacket(buf, buf.length, address, mTargetPort);
                mSocket.send(packet);
            } catch (Exception e) {
                Logger.w(TAG, e);
                disconnect();
                if (mListener != null) {
                    mListener.onError();
                }
                break;
            }
            readOffset += left;
        }
    }

    public void disconnect() {
        disconnect = true;
        try {
            mSocket.close();
            mSocket = null;
        } catch (Exception e) {
            Logger.w(TAG, e);
        }
    }

    public void setOnSendListener(OnSendListener listener) {
        mListener = listener;
    }

}

解码显示部分与在本机解码显示一致,不再赘述

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,639评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,277评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,221评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,474评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,570评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,816评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,957评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,718评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,176评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,511评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,646评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,322评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,934评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,755评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,987评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,358评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,514评论 2 348