Unity AVPro Video插件使用封装

链接:https://pan.baidu.com/s/1_dB2KxfSRtxJqeK1da-xqg
提取码:pwvj
先是NGUI
插件地址
把插件导入Unity Hierarchy框口右键

image.png

image.png

using UnityEngine;
using System.Collections;
using RenderHeads.Media.AVProVideo;
using System;
using System.IO;

public class AVProControl : MonoBehaviour
{
    MediaPlayer mediaPlayer;

    Transform UIRoot;
    GameObject btnPlay;
    GameObject btnStop;
    GameObject btnRePlay;
    UISlider movieSlider;
    /// <summary>
    /// 显示实际进度
    /// </summary>
    UILabel movieSliderTime;
    UISlider sourceSlider;
    UIToggle muteToggle;

    /// <summary>
    /// 当前视屏总长度毫秒
    /// </summary>
    float duration;
    //单位是秒 00:00
    float mediaPlayerValue;
    // Use this for initialization
    void Start()
    {
        mediaPlayer = GetComponent<MediaPlayer>();
        UIRoot = GameObject.Find("UI Root").transform;
        btnPlay = UIRoot.Find("btnPlay").gameObject;
        btnStop = UIRoot.Find("btnStop").gameObject;
        btnRePlay = UIRoot.Find("btnRePlay").gameObject;
        UIEventListener.Get(btnPlay).onClick += BtnPlayOnClick;
        UIEventListener.Get(btnStop).onClick += BtnStopOnClick;
        UIEventListener.Get(btnRePlay).onClick += BtnRePlayOnClick;

        movieSlider = UIRoot.Find("SliderMovie").GetComponent<UISlider>();
        UIEventListener.Get(movieSlider.gameObject).onPress += MovieSliderOnPress;
        movieSliderTime = movieSlider.transform.Find("time").GetComponent<UILabel>();
        sourceSlider = UIRoot.Find("SliderSource").GetComponent<UISlider>();
        UIEventListener.Get(sourceSlider.gameObject).onDrag += SourceSliderOnDrag;

        muteToggle = UIRoot.Find("MuteToggle").GetComponent<UIToggle>();
        UIEventListener.Get(muteToggle.gameObject).onClick += BtnMuteToggleOnClick;

        SetVideo("http://cn-sdqd-cu-v-02.acgvideo.com/upgcxcode/96/78/87367896/87367896-1-6.mp4?expires=1555985400&platform=html5&ssig=QTgvz85Ge3N6MFcaT3hMAA&oi=2008310406&trid=6d64c2c6756d4cfd866341415d9e383c&nfb=maPYqpoel5MI3qOUX6YpRA==&nfc=1");
        //SetVideo("chuanjianyinwei.mp4");
    }
    /// <summary>
    /// 设置音量大小
    /// </summary>
    private void SourceSliderOnDrag(GameObject go, Vector2 delta)
    {
        if (mediaPlayer.Control.IsPlaying())
        {
            mediaPlayer.Control.SetVolume(sourceSlider.value);
        }
    }

    /// <summary>
    /// 静音按钮设置
    /// </summary>
    /// <param name="go"></param>
    private void BtnMuteToggleOnClick(GameObject go)
    {
        mediaPlayer.Control.MuteAudio(muteToggle.value);
        // Debug.Log(muteToggle.value);
    }

    /// <summary>
    /// 拖动进度条设置视屏长度
    /// </summary>
    /// <param name="go"></param>
    /// <param name="state"></param>
    private void MovieSliderOnPress(GameObject go, bool state)
    {
        if (state == true)
        {
            mediaPlayer.Stop();
            //Debug.Log("PressEnter");
        }
        else
        {
            mediaPlayer.Control.Seek(movieSlider.value * mediaPlayer.Info.GetDurationMs());
            mediaPlayer.Play();
            //Debug.Log("PressExit");
        }
    }
    /// <summary>
    /// 加载视频
    /// </summary>
    /// <param name="path"></param>
    public void SetVideo(string path)
    {
        //网上或者本地加载
        if (path.Contains("http://"))
        {
            mediaPlayer.m_VideoLocation = MediaPlayer.FileLocation.AbsolutePathOrURL;
        }
        else
        {
            mediaPlayer.m_VideoLocation = MediaPlayer.FileLocation.RelativeToStreamingAssetsFolder;
            //mediaPlayer.OpenVideoFromFile(mediaPlayer.m_VideoLocation, path);
            //duration = mediaPlayer.Info.GetDurationMs();
            //durationSecond =TimeSpan.FromSeconds(duration * 0.001f).ToString().Substring(3,5); 
        }
        StartCoroutine(LoadVideoWithFading(path));
    }

    private IEnumerator LoadVideoWithFading(string path)
    {
        // Wait 3 frames for display object to update
        yield return new WaitForEndOfFrame();
        yield return new WaitForEndOfFrame();
        yield return new WaitForEndOfFrame();
        // Load the video
        if (Application.isPlaying)
        {
            if (!mediaPlayer.OpenVideoFromFile(mediaPlayer.m_VideoLocation, path))
            {
                Debug.LogError("Failed to open video!");
            }
            else
            {
                // Wait for the first frame to come through (could also use events for this)
                while (Application.isPlaying && (VideoIsReady(mediaPlayer) || AudioIsReady(mediaPlayer)))
                {
                    yield return null;
                }
                // Wait 3 frames for display object to update
                yield return new WaitForEndOfFrame();
                yield return new WaitForEndOfFrame();
                yield return new WaitForEndOfFrame();
            }

        }
        duration = mediaPlayer.Info.GetDurationMs();       

    }
    /// <summary>
    /// 视频是否准备好了
    /// </summary>
    /// <param name="mp"></param>
    /// <returns></returns>
    private bool VideoIsReady(MediaPlayer mp)
    {
        return (mp != null && mp.TextureProducer != null && mp.TextureProducer.GetTextureFrameCount() <= 0);
    }
    /// <summary>
    ///  声音是否准备好了
    /// </summary>
    /// <param name="mp"></param>
    /// <returns></returns>
    private bool AudioIsReady(MediaPlayer mp)
    {
        return (mp != null && mp.Control != null && mp.Control.CanPlay() && mp.Info.HasAudio() && !mp.Info.HasVideo());
    }
    /// <summary>
    /// 播放
    /// </summary>
    /// <param name="go"></param>
    private void BtnPlayOnClick(GameObject go)
    {
        mediaPlayer.Play();
    }
    /// <summary>
    /// 暂停
    /// </summary>
    /// <param name="go"></param>
    private void BtnStopOnClick(GameObject go)
    {
        mediaPlayer.Stop();
    }
    /// <summary>
    /// 重播
    /// </summary>
    /// <param name="go"></param>
    private void BtnRePlayOnClick(GameObject go)
    {
        mediaPlayer.Rewind(false);
        mediaPlayer.Play();
    }
    // Update is called once per frame
    void Update()
    {
        SetSliderValue();
    }
    /// <summary>
    /// 进度条与视频进度百分比同步
    /// </summary>
    void SetSliderValue()
    {
        if (mediaPlayer.Control.IsPlaying())
        {
            //当前在几毫秒
            float time = mediaPlayer.Control.GetCurrentTimeMs();
            //Debug.Log(time+"_"+duration);
            //最少取0.1    
            //print(Helper.GetTimeString(time * 0.001f));
            //TimeSpan ts = TimeSpan.FromSeconds(time * 0.001f);
            //插件自带 把时间转化为00:00
            movieSliderTime.text = Helper.GetTimeString(time * 0.001f) + "/" + Helper.GetTimeString(duration * 0.001f);
            mediaPlayerValue = Mathf.Clamp(time / duration, 0.0f, 1.0f);
            movieSlider.value = mediaPlayerValue;
        }
    }
}
image.png

NGUI按照这样创建就可以了
不过加载网上资源的缓存条不知道在哪里调用

然后UGUI 我把AVpro封装了一下 这个类不负责UI逻辑 所以NGUI UGUI GUI都可以用

using UnityEngine;
using System.Collections;
using RenderHeads.Media.AVProVideo;
using System;
using System.IO;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using System.Text;
using System.Security.Cryptography;

public class AVProControl
{
    MediaPlayer mediaPlayer;
    /// <summary>
    /// 当前视屏总长度毫秒
    /// </summary>
    float duration;

    public AVProControl(MediaPlayer mediaPlayer)
    {
        this.mediaPlayer = mediaPlayer;
    }

    public void Play()
    {
        mediaPlayer.Play();
    }

    public void Stop()
    {
        if (isPlaying())
        {
            mediaPlayer.Stop();
        }
    }

    public void RePlay()
    {
        mediaPlayer.Rewind(false);
        mediaPlayer.Play();
    }

    /// <summary>
    /// 设置速率
    /// </summary>
    public void SetSpeedRate(float val)
    {
        mediaPlayer.Control.SetPlaybackRate(val);
    }

    /// <summary>
    /// 获取总时间00:00格式
    /// </summary>
    /// <returns></returns>
    public string GetTotalProgressTime()
    {
            return Helper.GetTimeString(duration * 0.001f);
    }

    /// <summary>
    /// 获取当前时间00:00格式
    /// </summary>
    /// <returns></returns>
    public string GetNowProgressTime()
    {
            //当前在几毫秒
            float time = mediaPlayer.Control.GetCurrentTimeMs();
            //最少取0.1    
            TimeSpan ts = TimeSpan.FromSeconds(time * 0.001f);
            //插件自带 把时间转化为00:00
            return Helper.GetTimeString(time * 0.001f);
    }

    /// <summary>
    /// 获取进度条0-1
    /// </summary>
    /// <returns></returns>
    public float GetProgress()
    {
            //当前在几毫秒
            float time = mediaPlayer.Control.GetCurrentTimeMs();
            return Mathf.Clamp(time / duration, 0.0f, 1.0f);
    }
    /// <summary>
    /// 设置进度条
    /// </summary>
    /// <param name="value">0-1</param>
    public void SetProgress(float value)
    {
        mediaPlayer.Control.Seek(value * mediaPlayer.Info.GetDurationMs());
    }
    /// <summary>
    /// 设置音量大小
    /// </summary>
    /// <param name="value">0-1</param>
    public void SetVolume(float value)
    {
        if (isPlaying())
        {
            mediaPlayer.Control.SetVolume(value);
        }
    }

    /// <summary>
    /// 静音按钮设置
    /// </summary>
    /// <param name="go"></param>
    public void SetMute(bool isMute)
    {
        if (isPlaying())
        {
            mediaPlayer.Control.MuteAudio(isMute);
        }
    }

    /// <summary>
    /// 加载加密视频
    /// </summary>
    /// <param name="path"></param>
    public IEnumerator LoadEncryptVideoWithFading(string path)
    {
        string newPath = Application.dataPath + "/StreamingAssets/videoNew";
        path = newPath + "/" + path;

        byte[] enBytes = DecryptVideo(newPath, FileTools.ReadFile(path));

        // Wait 3 frames for display object to update
        yield return new WaitForEndOfFrame();
        yield return new WaitForEndOfFrame();
        yield return new WaitForEndOfFrame();
        // Load the video
        if (Application.isPlaying)
        {
            if (!mediaPlayer.OpenVideoFromBuffer(enBytes))
            {
                Debug.LogError("Failed to open video!");
            }
            else
            {
                // Wait for the first frame to come through (could also use events for this)
                while (Application.isPlaying && (VideoIsReady(mediaPlayer) || AudioIsReady(mediaPlayer)))
                {
                    yield return null;
                }
                // Wait 3 frames for display object to update
                yield return new WaitForEndOfFrame();
                yield return new WaitForEndOfFrame();
                yield return new WaitForEndOfFrame();
            }

        }
        duration = mediaPlayer.Info.GetDurationMs();

    }

    /// <summary>
    /// 加载普通视频
    /// </summary>
    /// <param name="path"></param>
    /// <returns></returns>
    public IEnumerator LoadVideoWithFading(string path)
    {
        //网上或者本地加载
        if (path.Contains("http://"))
        {
            mediaPlayer.m_VideoLocation = MediaPlayer.FileLocation.AbsolutePathOrURL;
        }
        else
        {
            mediaPlayer.m_VideoLocation = MediaPlayer.FileLocation.RelativeToStreamingAssetsFolder;
        }
        // Wait 3 frames for display object to update
        yield return new WaitForEndOfFrame();
        yield return new WaitForEndOfFrame();
        yield return new WaitForEndOfFrame();
        // Load the video
        if (Application.isPlaying)
        {
            if (!mediaPlayer.OpenVideoFromFile(mediaPlayer.m_VideoLocation, path))
            {
                Debug.LogError("Failed to open video!");
            }
            else
            {
                // Wait for the first frame to come through (could also use events for this)
                while (Application.isPlaying && (VideoIsReady(mediaPlayer) || AudioIsReady(mediaPlayer)))
                {
                    yield return null;
                }
                // Wait 3 frames for display object to update
                yield return new WaitForEndOfFrame();
                yield return new WaitForEndOfFrame();
                yield return new WaitForEndOfFrame();
            }

        }
        duration = mediaPlayer.Info.GetDurationMs();

    }
    /// <summary>
    /// 视频是否准备好了
    /// </summary>
    /// <param name="mp"></param>
    /// <returns></returns>
    private bool VideoIsReady(MediaPlayer mp)
    {
        return (mp != null && mp.TextureProducer != null && mp.TextureProducer.GetTextureFrameCount() <= 0);
    }
    /// <summary>
    ///  声音是否准备好了
    /// </summary>
    /// <param name="mp"></param>
    /// <returns></returns>
    private bool AudioIsReady(MediaPlayer mp)
    {
        return (mp != null && mp.Control != null && mp.Control.CanPlay() && mp.Info.HasAudio() && !mp.Info.HasVideo());
    }
    /// <summary>
    /// 是否在播放
    /// </summary>
    /// <returns></returns>
    public bool isPlaying()
    {
        return mediaPlayer.Control.IsPlaying();
    }

    /// <summary>
    /// 解密
    /// </summary>
    /// <param name="KeyPath"></param>
    /// <param name="data"></param>
    /// <returns></returns>
    public byte[] DecryptVideo(string KeyPath, byte[] data)
    {
        Rijndael rij = new Rijndael();

        RijndaelKey rijKey = rij.GetKeyAndIV(KeyPath);
        return rij.Decrypt(data, rijKey.key, rijKey.IV);
    }
}

UGUI挂的是这个脚本


image.png

核心调用脚本

  private void Update()
    {
        if (AVpro.isPlaying())
        {
            movieSlider.value = AVpro.GetProgress();
        }
    }

    public void InitAvPro()
    {
        AVpro = new AVProControl(transform.Find("Video/AVPro Video").GetComponent<DisplayUGUI>()._mediaPlayer);
        Button btnPlay = transform.Find("Video/play").GetComponent<Button>();
        var spStart = btnPlay.GetComponent<Image>().sprite;
        btnPlay.onClick.AddListener(() =>
        {
            if (AVpro.isPlaying())
            {
                btnPlay.GetComponent<Image>().sprite = btnPlay.spriteState.disabledSprite;
                AVpro.Stop();
            }
            else
            {
                btnPlay.GetComponent<Image>().sprite = spStart;
                AVpro.Play();
            }

        });

        transform.Find("Video/replay").GetComponent<Button>().onClick.AddListener(() =>
        {
            AVpro.RePlay();
        });

        //设置拖动进度条
        movieSlider = transform.Find("Video/Slider").GetComponent<Slider>();
        EventTriggerListener.Get(movieSlider.gameObject).onDown += ((go) =>
        {
            AVpro.Stop();
        });

        EventTriggerListener.Get(movieSlider.gameObject).onUp += ((go) =>
        {
            AVpro.SetProgress(movieSlider.value);
            AVpro.Play();
        });
    }

这个是加密解密类 详细的可以看
https://www.jianshu.com/p/02cfd3b979cb

using UnityEngine;
using System.Collections;
using System;
using System.Security.Cryptography;
using System.Text;
public struct RijndaelKey
{
    public byte[] key;
    public byte[] IV;

    public RijndaelKey(byte[] key, byte[] iV)
    {
        this.key = key;
        IV = iV;
    }
}

public class Rijndael
{
    /// <summary>
    /// Rijndael加密
    /// </summary>
    /// <param name="plainData">密文</param>
    /// <param name="key">密钥</param>
    /// <param name="IV">向量</param>
    /// <returns></returns>
    public byte[] Encrypt(byte[] plainData, byte[] key, byte[] IV)
    {
        if (plainData == null || plainData.Length <= 0)
            throw new ArgumentNullException("video");
        if (key == null || key.Length <= 0)
            throw new ArgumentNullException("Key");
        if (IV == null || IV.Length <= 0)
            throw new ArgumentNullException("IV");

        byte[] encrypted;
        //创建RijndaelManaged对象
        //使用指定的键和IV。
        //用完就会被释放
        using (RijndaelManaged rijAlg = new RijndaelManaged())
        {
            rijAlg.Key = key;
            rijAlg.IV = IV;
            //设置对称算法的运算模式
            rijAlg.Mode = CipherMode.ECB;
            //设置填充模式
            rijAlg.Padding = PaddingMode.ISO10126;

            //创建加密程序以执行流转换。
            ICryptoTransform encryptor = rijAlg.CreateEncryptor(rijAlg.Key, rijAlg.IV);
            encrypted = encryptor.TransformFinalBlock(plainData, 0, plainData.Length);
        }
        //从内存流返回加密的字节。
        return encrypted;
    }
    /// <summary>
    /// 解密
    /// </summary>
    /// <param name="cipherData"></param>
    /// <param name="key"></param>
    /// <param name="IV"></param>
    /// <returns></returns>
    public byte[] Decrypt(byte[] cipherData, byte[] key, byte[] IV)
    {
        if (cipherData == null || cipherData.Length <= 0)
            throw new ArgumentNullException("video");
        if (key == null || key.Length <= 0)
            throw new ArgumentNullException("Key");
        if (IV == null || IV.Length <= 0)
            throw new ArgumentNullException("IV");

        byte[] decrypted;
        using (RijndaelManaged rijAlg = new RijndaelManaged())
        {
            rijAlg.Key = key;
            rijAlg.IV = IV;
            rijAlg.Mode = CipherMode.ECB;
            rijAlg.Padding = PaddingMode.ISO10126;
            //创建解密程序以执行流转换。
            ICryptoTransform decryptor = rijAlg.CreateDecryptor(rijAlg.Key, rijAlg.IV);
            decrypted = decryptor.TransformFinalBlock(cipherData, 0, cipherData.Length);
        }
        return decrypted;
    }

    /// <summary>
    /// 创建Key 和IV在某个目录下
    /// </summary>
    /// <param name="path"></param>
    /// <returns></returns>
    public RijndaelKey CreateKeyAndIV(string path)
    {
        RijndaelManaged rijma = new RijndaelManaged();
        rijma.GenerateKey();
        rijma.GenerateIV();
        FileTools.CreateFile(path + "/keyTxt.txt", rijma.Key);
        FileTools.CreateFile(path + "/IVTxt.txt", rijma.IV);

        return new RijndaelKey(rijma.Key, rijma.IV);
    }

    /// <summary>
    /// 获取Key 和IV
    /// </summary>
    /// <param name="path"></param>
    /// <returns></returns>
    public RijndaelKey GetKeyAndIV(string path)
    {
        byte[] key = FileTools.ReadFile(path + "/keyTxt.txt");
        byte[] IV = FileTools.ReadFile(path + "/IVTxt.txt");
        return new RijndaelKey(key, IV);
    }
}
using System.Collections;
using System.IO;
using System.Text;
/// <summary>
/// IO读写类
/// </summary>
public class FileTool
{
    /// <summary>
    /// 创建目录文件夹 有就不创建
    /// </summary>
    public static void CreateDirectory(string filePath)
    {
        if (!string.IsNullOrEmpty(filePath))
        {
            if (!File.Exists(filePath))
            {
                Directory.CreateDirectory(filePath);
            }
        }
    }
    /// <summary>
    /// 创建文件
    /// </summary>
    /// <param name="filePath"></param>
    /// <param name="bytes"></param>
    public static void CreateFile(string filePath, byte[] bytes)
    {
        using (FileStream fs = new FileStream(filePath, FileMode.Create, FileAccess.Write))
        {
            fs.Write(bytes, 0, bytes.Length);
        }
    }
    public static void CreateFile(string filePath, string str)
    {
        byte[] bytes = StrToBytes(str);
        using (FileStream fs = new FileStream(filePath, FileMode.Create, FileAccess.Write))
        {
            fs.Write(bytes, 0, bytes.Length);
        }
    }
    /// <summary>
    /// 读取文件
    /// </summary>
    /// <param name="filePath"></param>
    /// <returns></returns>
    public static byte[] ReadFile(string filePath)
    {
        using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read))
        {
            byte[] infbytes = new byte[fs.Length];
            fs.Read(infbytes, 0, infbytes.Length);
            return infbytes;
        }
    }

    public static byte[] StrToBytes(string str)
    {
        return Encoding.Default.GetBytes(str);
    }

    public static string BytesToStr(byte[] bytes)
    {
        return Encoding.Default.GetString(bytes, 0, bytes.Length);
    }
}

GitHub
https://github.com/1004019267/AVPro-Video

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

推荐阅读更多精彩内容