Bläddra i källkod

添加m3u8下载服务和本地记录视频播放位置组件

杨充 4 år sedan
förälder
incheckning
5e43fb8684
96 ändrade filer med 7472 tillägg och 42 borttagningar
  1. 11 10
      Demo/build.gradle
  2. 6 0
      Demo/proguard-rules.pro
  3. 10 1
      Demo/src/main/AndroidManifest.xml
  4. 7 0
      Demo/src/main/java/com/yc/ycvideoplayer/MainActivity.java
  5. 223 0
      Demo/src/main/java/com/yc/ycvideoplayer/m3u8/M3u8Activity.java
  6. 4 1
      Demo/src/main/java/com/yc/ycvideoplayer/newPlayer/activity/NormalActivity.java
  7. 94 0
      Demo/src/main/res/layout/activity_m3u8_view.xml
  8. 8 0
      Demo/src/main/res/layout/activity_main.xml
  9. 1 0
      VideoM3u8/.gitignore
  10. 25 0
      VideoM3u8/build.gradle
  11. BIN
      VideoM3u8/libs/commons-io-2.5.jar
  12. 25 0
      VideoM3u8/proguard-rules.pro
  13. 16 0
      VideoM3u8/src/main/AndroidManifest.xml
  14. 103 0
      VideoM3u8/src/main/java/com/yc/m3u8/bean/M3u8.java
  15. 72 0
      VideoM3u8/src/main/java/com/yc/m3u8/bean/M3u8Ts.java
  16. 24 0
      VideoM3u8/src/main/java/com/yc/m3u8/inter/BaseListener.java
  17. 31 0
      VideoM3u8/src/main/java/com/yc/m3u8/inter/DownLoadListener.java
  18. 26 0
      VideoM3u8/src/main/java/com/yc/m3u8/inter/M3U8Listener.java
  19. 37 0
      VideoM3u8/src/main/java/com/yc/m3u8/inter/OnDownloadListener.java
  20. 20 0
      VideoM3u8/src/main/java/com/yc/m3u8/inter/OnM3u8InfoListener.java
  21. 106 0
      VideoM3u8/src/main/java/com/yc/m3u8/manager/M3u8InfoManger.java
  22. 363 0
      VideoM3u8/src/main/java/com/yc/m3u8/manager/M3u8LiveManger.java
  23. 385 0
      VideoM3u8/src/main/java/com/yc/m3u8/manager/M3u8Manger.java
  24. 389 0
      VideoM3u8/src/main/java/com/yc/m3u8/task/M3u8DownloadTask.java
  25. 205 0
      VideoM3u8/src/main/java/com/yc/m3u8/utils/M3u8FileUtils.java
  26. 47 0
      VideoM3u8/src/main/java/com/yc/m3u8/utils/NetSpeedUtils.java
  27. 3 0
      VideoM3u8/src/main/res/values/strings.xml
  28. 2 0
      VideoPlayer/proguard-rules.pro
  29. 11 1
      VideoPlayer/src/main/java/com/yc/video/bridge/ControlWrapper.java
  30. 5 1
      VideoPlayer/src/main/java/com/yc/video/config/ConstantKeys.java
  31. 55 0
      VideoPlayer/src/main/java/com/yc/video/config/VideoPlayerConfig.java
  32. 3 1
      VideoPlayer/src/main/java/com/yc/video/controller/BaseVideoController.java
  33. 12 0
      VideoPlayer/src/main/java/com/yc/video/player/InterVideoPlayer.java
  34. 17 5
      VideoPlayer/src/main/java/com/yc/video/player/VideoPlayer.java
  35. 82 16
      VideoPlayer/src/main/java/com/yc/video/ui/view/BasisVideoController.java
  36. 39 0
      VideoPlayer/src/main/java/com/yc/video/ui/view/CustomBottomView.java
  37. 12 0
      VideoPlayer/src/main/java/com/yc/video/ui/view/CustomErrorView.java
  38. 2 1
      VideoPlayer/src/main/java/com/yc/video/ui/view/CustomGestureView.java
  39. 1 0
      VideoPlayer/src/main/java/com/yc/video/ui/view/CustomLiveControlView.java
  40. 173 0
      VideoPlayer/src/main/java/com/yc/video/ui/view/CustomOncePlayView.java
  41. 1 0
      VideoPlayer/src/main/java/com/yc/video/ui/view/CustomPrepareView.java
  42. 1 1
      VideoPlayer/src/main/java/com/yc/video/ui/view/CustomTitleView.java
  43. 1 1
      VideoPlayer/src/main/res/layout/custom_video_player_completed.xml
  44. 3 3
      VideoPlayer/src/main/res/layout/custom_video_player_error.xml
  45. 50 0
      VideoPlayer/src/main/res/layout/custom_video_player_once_live.xml
  46. 1 0
      VideoSqlHelper/.gitignore
  47. 27 0
      VideoSqlHelper/build.gradle
  48. 0 0
      VideoSqlHelper/consumer-rules.pro
  49. 21 0
      VideoSqlHelper/proguard-rules.pro
  50. 7 0
      VideoSqlHelper/src/main/AndroidManifest.xml
  51. 33 0
      VideoSqlHelper/src/main/java/com/yc/database/annotation/Column.java
  52. 22 0
      VideoSqlHelper/src/main/java/com/yc/database/annotation/NotDBColumn.java
  53. 33 0
      VideoSqlHelper/src/main/java/com/yc/database/annotation/PrimaryKey.java
  54. 28 0
      VideoSqlHelper/src/main/java/com/yc/database/annotation/Table.java
  55. 53 0
      VideoSqlHelper/src/main/java/com/yc/database/bean/BindSQL.java
  56. 62 0
      VideoSqlHelper/src/main/java/com/yc/database/bean/EntityTable.java
  57. 26 0
      VideoSqlHelper/src/main/java/com/yc/database/bean/PrimaryKey.java
  58. 185 0
      VideoSqlHelper/src/main/java/com/yc/database/bean/Property.java
  59. 31 0
      VideoSqlHelper/src/main/java/com/yc/database/listener/InterDBListener.java
  60. 27 0
      VideoSqlHelper/src/main/java/com/yc/database/listener/SimpleDBListener.java
  61. 270 0
      VideoSqlHelper/src/main/java/com/yc/database/manager/EntityTableManager.java
  62. 191 0
      VideoSqlHelper/src/main/java/com/yc/database/manager/FieldTypeManager.java
  63. 286 0
      VideoSqlHelper/src/main/java/com/yc/database/manager/SQLExecuteManager.java
  64. 382 0
      VideoSqlHelper/src/main/java/com/yc/database/sql/SQLBuilder.java
  65. 122 0
      VideoSqlHelper/src/main/java/com/yc/database/sql/SQLiteContext.java
  66. 462 0
      VideoSqlHelper/src/main/java/com/yc/database/sql/SQLiteDB.java
  67. 119 0
      VideoSqlHelper/src/main/java/com/yc/database/sql/SQLiteDBConfig.java
  68. 73 0
      VideoSqlHelper/src/main/java/com/yc/database/sql/SQLiteDBFactory.java
  69. 136 0
      VideoSqlHelper/src/main/java/com/yc/database/sql/SQLiteHelper.java
  70. 152 0
      VideoSqlHelper/src/main/java/com/yc/database/utils/CursorUtil.java
  71. 81 0
      VideoSqlHelper/src/main/java/com/yc/database/utils/DBLog.java
  72. 407 0
      VideoSqlHelper/src/main/java/com/yc/database/utils/DateUtil.java
  73. 119 0
      VideoSqlHelper/src/main/java/com/yc/database/utils/FieldUtil.java
  74. 18 0
      VideoSqlHelper/src/main/java/com/yc/database/utils/ValueUtil.java
  75. 1 0
      VideoSqlLite/.gitignore
  76. 25 0
      VideoSqlLite/build.gradle
  77. 0 0
      VideoSqlLite/consumer-rules.pro
  78. 21 0
      VideoSqlLite/proguard-rules.pro
  79. 5 0
      VideoSqlLite/src/main/AndroidManifest.xml
  80. 78 0
      VideoSqlLite/src/main/java/com/yc/videosqllite/cache/InterCache.java
  81. 342 0
      VideoSqlLite/src/main/java/com/yc/videosqllite/cache/SystemLruCache.java
  82. 205 0
      VideoSqlLite/src/main/java/com/yc/videosqllite/cache/VideoLruCache.java
  83. 121 0
      VideoSqlLite/src/main/java/com/yc/videosqllite/cache/VideoMapCache.java
  84. 12 0
      VideoSqlLite/src/main/java/com/yc/videosqllite/dao/SqlLiteCache.java
  85. 72 0
      VideoSqlLite/src/main/java/com/yc/videosqllite/manager/CacheConfig.java
  86. 104 0
      VideoSqlLite/src/main/java/com/yc/videosqllite/manager/LocationManager.java
  87. 90 0
      VideoSqlLite/src/main/java/com/yc/videosqllite/model/VideoLocation.java
  88. 109 0
      VideoSqlLite/src/main/java/com/yc/videosqllite/utils/VideoMd5Utils.java
  89. BIN
      image/直播服务架构图.png
  90. BIN
      image/视频直播流程图.png
  91. 97 0
      read/09.视频深度优化处理.md
  92. 0 0
      read/30.视频播放器使用设计模式.md
  93. 60 0
      read/51.直播基础知识点介绍.md
  94. 21 0
      read/52.直播推流端分析.md
  95. 21 0
      read/53.直播播放端分析.md
  96. 3 0
      settings.gradle

+ 11 - 10
Demo/build.gradle

@@ -59,21 +59,22 @@ dependencies {
     implementation 'com.github.ctiao:ndkbitmap-armv7a:0.9.21'
 
 
-//    implementation project(path: ':VideoCache')
-//    implementation project(path: ':VideoPlayer')
-//    implementation project(path: ':VideoKernel')
-//    implementation project(path: ':VideoView')
+    implementation project(path: ':VideoCache')
+    implementation project(path: ':VideoPlayer')
+    implementation project(path: ':VideoKernel')
+    implementation project(path: ':VideoView')
     implementation project(path: ':MusicPlayer')
+    implementation project(path: ':VideoM3u8')
 
-    //implementation 'cn.yc:MusicPlayer:1.0.0'
-    implementation 'cn.yc:VideoPlayer:3.0.9'
-    implementation 'cn.yc:VideoCache:3.0.5'
-    implementation 'cn.yc:VideoKernel:3.0.5'
-    implementation 'cn.yc:VideoView:3.0.5'
+//    implementation 'cn.yc:MusicPlayer:1.0.0'
+//    implementation 'cn.yc:VideoPlayer:3.0.9'
+//    implementation 'cn.yc:VideoCache:3.0.5'
+//    implementation 'cn.yc:VideoKernel:3.0.5'
+//    implementation 'cn.yc:VideoView:3.0.5'
 
     //自己封装的库,都有对应的案例项目【欢迎star】:https://github.com/yangchong211
     implementation 'cn.yc:YCStatusBarLib:1.5.0'
     implementation 'com.yc:PagerLib:1.0.4'
-    implementation 'cn.yc:YCStateLib:1.2.2'                                 
+    implementation 'cn.yc:YCStateLib:1.2.2'
 
 }

+ 6 - 0
Demo/proguard-rules.pro

@@ -19,3 +19,9 @@
 # If you keep the line number information, uncomment this to
 # hide the original source file name.
 #-renamesourcefileattribute SourceFile
+
+
+#ijkplayer
+-keep class tv.danmaku.ijk.media.player.** {*;}
+-keep class tv.danmaku.ijk.media.player.IjkMediaPlayer{*;}
+-keep class tv.danmaku.ijk.media.player.ffmpeg.FFmpegApi{*;}

+ 10 - 1
Demo/src/main/AndroidManifest.xml

@@ -27,8 +27,16 @@
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity>
+        <!--<activity android:name="com.yc.ycvideoplayer.MainActivity"
+            android:configChanges="orientation|keyboardHidden|screenSize"
+            android:screenOrientation="portrait">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
 
-
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+        <activity android:name="com.yc.ycvideoplayer.newPlayer.activity.TypeActivity"/>-->
         <activity android:name="com.yc.ycvideoplayer.oldPlayer.TestTinyActivity"
             android:configChanges="orientation|keyboardHidden|screenSize"
             android:screenOrientation="portrait"/>
@@ -118,6 +126,7 @@
             android:configChanges="orientation|keyboardHidden|screenSize"
             android:screenOrientation="portrait"/>
         <activity android:name="com.yc.ycvideoplayer.music.MusicPlayerActivity"/>
+        <activity android:name="com.yc.ycvideoplayer.m3u8.M3u8Activity"/>
     </application>
 
 </manifest>

+ 7 - 0
Demo/src/main/java/com/yc/ycvideoplayer/MainActivity.java

@@ -21,6 +21,7 @@ import com.yc.music.model.AudioBean;
 import com.yc.music.service.PlayService;
 import com.yc.music.tool.BaseAppHelper;
 import com.yc.ycvideoplayer.demo.DemoActivity;
+import com.yc.ycvideoplayer.m3u8.M3u8Activity;
 import com.yc.ycvideoplayer.music.MusicPlayerActivity;
 import com.yc.ycvideoplayer.newPlayer.activity.TypeActivity;
 import com.yc.ycvideoplayer.oldPlayer.OldActivity;
@@ -44,6 +45,7 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
     private TextView mTv2;
     private TextView mTv3;
     private TextView mTv4;
+    private TextView mTv5;
     private PlayServiceConnection mPlayServiceConnection;
 
 
@@ -68,11 +70,13 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
         mTv2 = (TextView) findViewById(R.id.tv_2);
         mTv3 = (TextView) findViewById(R.id.tv_3);
         mTv4 = (TextView) findViewById(R.id.tv_4);
+        mTv5 = (TextView) findViewById(R.id.tv_5);
 
         mTv1.setOnClickListener(this);
         mTv2.setOnClickListener(this);
         mTv3.setOnClickListener(this);
         mTv4.setOnClickListener(this);
+        mTv5.setOnClickListener(this);
     }
 
     @Override
@@ -91,6 +95,9 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
                 startCheckService();
                 startActivity(MusicPlayerActivity.class);
                 break;
+            case R.id.tv_5:
+                startActivity(M3u8Activity.class);
+                break;
         }
     }
 

+ 223 - 0
Demo/src/main/java/com/yc/ycvideoplayer/m3u8/M3u8Activity.java

@@ -0,0 +1,223 @@
+package com.yc.ycvideoplayer.m3u8;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+import android.widget.EditText;
+import android.widget.TextView;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+import com.yc.kernel.utils.VideoLogUtils;
+import com.yc.m3u8.bean.M3u8;
+import com.yc.m3u8.inter.OnDownloadListener;
+import com.yc.m3u8.inter.OnM3u8InfoListener;
+import com.yc.m3u8.manager.M3u8InfoManger;
+import com.yc.m3u8.manager.M3u8LiveManger;
+import com.yc.m3u8.task.M3u8DownloadTask;
+import com.yc.m3u8.utils.M3u8FileUtils;
+import com.yc.m3u8.utils.NetSpeedUtils;
+import com.yc.ycvideoplayer.newPlayer.activity.NormalActivity;
+
+import org.yc.ycvideoplayer.R;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+
+public class M3u8Activity extends AppCompatActivity {
+    //url随时可能失效
+    private String url = "yangchong";
+    private TextView tvSpeed1;
+    private EditText etUrl;
+    private TextView tvConsole;
+    private TextView tvSaveFilePathTip;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_m3u8_view);
+        tvSpeed1 = (TextView) findViewById(R.id.tv_speed1);
+        etUrl = (EditText) findViewById(R.id.et_url);
+        etUrl.setText(url);
+        tvConsole = (TextView) findViewById(R.id.tv_console);
+        tvSaveFilePathTip= (TextView) findViewById(R.id.tv_savepath_tip);
+    }
+
+
+    public void onGetInfo(View view) {
+        String url = etUrl.getText().toString().trim();
+        M3u8InfoManger.getInstance().getM3U8Info(url, new OnM3u8InfoListener() {
+            @Override
+            public void onSuccess(M3u8 m3U8) {
+                tvConsole.append("\n\n获取成功了" + m3U8);
+                VideoLogUtils.e("获取成功了" + m3U8);
+            }
+
+            @Override
+            public void onStart() {
+                tvConsole.append("\n\n开始获取信息");
+                VideoLogUtils.e("开始获取信息");
+            }
+
+            @Override
+            public void onError(Throwable errorMsg) {
+                tvConsole.append("\n\n出错了" + errorMsg);
+                VideoLogUtils.e("出错了" + errorMsg);
+            }
+        });
+    }
+
+    //上一秒的大小
+    private long lastLength = 0;
+    M3u8DownloadTask task1 = new M3u8DownloadTask("1001");
+
+    public void onDownload(View view) {
+        String url = etUrl.getText().toString().trim();
+//        url = etUrl.getText().toString();
+        task1.setSaveFilePath("/sdcard/111/" + System.currentTimeMillis() + ".ts");
+        tvSaveFilePathTip.setText("文件保存在:/sdcard/111/" + System.currentTimeMillis() + ".ts");
+        task1.download(url, new OnDownloadListener() {
+            @Override
+            public void onDownloading(final long itemFileSize, final int totalTs, final int curTs) {
+                VideoLogUtils.e(task1.getTaskId() + "下载中.....itemFileSize=" + itemFileSize + "\ttotalTs=" + totalTs + "\tcurTs=" + curTs);
+                tvConsole.append("\n\n下载中....." + itemFileSize + "\t" + totalTs + "\t" + curTs);
+            }
+
+            /**
+             * 下载成功
+             */
+            @Override
+            public void onSuccess() {
+                VideoLogUtils.e(task1.getTaskId() + "下载完成了");
+                tvConsole.append("\n\n下载完成");
+            }
+
+            /**
+             * 当前的进度回调
+             *
+             * @param curLenght
+             */
+            @Override
+            public void onProgress(final long curLenght) {
+                if (curLenght - lastLength > 0) {
+                    final String speed = NetSpeedUtils.getInstance().displayFileSize(curLenght - lastLength) + "/s";
+                    VideoLogUtils.e(task1.getTaskId() + "speed = " + speed);
+                    runOnUiThread(new Runnable() {
+                        @Override
+                        public void run() {
+                            VideoLogUtils.e("更新了");
+                            tvSpeed1.setText(speed);
+                            VideoLogUtils.e(tvSpeed1.getText().toString());
+                        }
+                    });
+                    lastLength = curLenght;
+
+                }
+            }
+
+            @Override
+            public void onStart() {
+                VideoLogUtils.e(task1.getTaskId() + "开始下载了");
+                tvConsole.append("\n\n开始下载");
+            }
+
+            @Override
+            public void onError(Throwable errorMsg) {
+                tvConsole.append("\n\n出错了" + errorMsg);
+                VideoLogUtils.e(task1.getTaskId() + "出错了" + errorMsg);
+            }
+        });
+    }
+
+    public void onStopTask1(View view) {
+        task1.stop();
+        M3u8LiveManger.getInstance().stop();
+    }
+
+    /**
+     * 当前正在下载的视频
+     */
+    private int curTsIndex;
+
+    public void onLiveDownload(View view) {
+//        String url = "http://tvbilive7-i.akamaihd.net/hls/live/494651/CJHK4/CJHK4-06.m3u8";
+        String url = etUrl.getText().toString().trim();
+        String toFile="/sdcard/" + System.currentTimeMillis() + ".ts";
+        tvSaveFilePathTip.setText("缓存目录在:/sdcard/11m3u8/\n最终导出的缓存文件在:"+toFile);
+        M3u8LiveManger.getInstance()
+                .setTempDir("/sdcard/11m3u8/")
+                .setSaveFile(toFile)//(设置导出缓存文件)必须以.ts结尾
+                .caching(url, new OnDownloadListener() {
+                    @Override
+                    public void onDownloading(long itemFileSize, int totalTs, int curTs) {
+                        curTsIndex = curTs;
+                        tvConsole.append(String.format("\n\n下载中.....开始下载第 %s 个视频了", curTs));
+//                        tvConsole.setText("第 " + curTs + " 个视频下载中\n\n" + tvConsole.getText().toString());
+                    }
+
+                    @Override
+                    public void onSuccess() {
+
+                    }
+
+                    @Override
+                    public void onProgress(long curLength) {
+                        if (curLength - lastLength > 0) {
+                            final String speed = NetSpeedUtils.getInstance().displayFileSize(curLength - lastLength) + "/s";
+                            VideoLogUtils.e(task1.getTaskId() + "speed = " + speed);
+                            runOnUiThread(new Runnable() {
+                                @Override
+                                public void run() {
+                                    VideoLogUtils.e("更新了");
+                                    tvSpeed1.setText(speed + "( 第" + (curTsIndex + 1) + "个视频 )");
+                                    VideoLogUtils.e(tvSpeed1.getText().toString());
+                                }
+                            });
+                            lastLength = curLength;
+                        }
+                    }
+
+                    @Override
+                    public void onStart() {
+                        tvConsole.append("\n\n开始缓存");
+                    }
+
+                    @Override
+                    public void onError(Throwable errorMsg) {
+                        tvConsole.append("\n\n缓存出错了" + errorMsg);
+                    }
+                });
+    }
+
+    public void onGetLiveCache(View view) {
+        String currentTs = M3u8LiveManger.getInstance().getCurrentTs();
+        tvConsole.append("\n\n缓存完成了,已经存至:" + currentTs);
+        Log.e("hdltag", "onGetLiveCache(Main2Activity.java:151): currentTs = " + currentTs);
+    }
+
+    public void onPlay(View view){
+        String url = etUrl.getText().toString().trim();
+        Intent intent = new Intent(this, NormalActivity.class);
+        intent.putExtra("url",url);
+        startActivity(intent);
+    }
+
+    public void onMergin(View view) {
+        File dir=new File("/sdcard/11m3u8/11");
+        File[] files = dir.listFiles();
+        List<File> fileList=new ArrayList<>();
+        for (File file : files) {
+            fileList.add(file);
+        }
+        try {
+            M3u8FileUtils.merge(fileList,"/sdcard/1123/"+ System.currentTimeMillis()+".ts");
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+
+    }
+}

+ 4 - 1
Demo/src/main/java/com/yc/ycvideoplayer/newPlayer/activity/NormalActivity.java

@@ -98,7 +98,10 @@ public class NormalActivity extends AppCompatActivity implements View.OnClickLis
     }
 
     private void initVideoPlayer() {
-        String url = ConstantVideo.VideoPlayerList[0];
+        String url = getIntent().getStringExtra(IntentKeys.URL);
+        if (url==null || url.length()==0){
+            url = ConstantVideo.VideoPlayerList[0];
+        }
         //创建基础视频播放器,一般播放器的功能
         controller = new BasisVideoController(this);
         //设置控制器

+ 94 - 0
Demo/src/main/res/layout/activity_m3u8_view.xml

@@ -0,0 +1,94 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+    <EditText
+        android:id="@+id/et_url"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:hint="下载地址"
+        android:singleLine="true" />
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal">
+
+        <Button
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:onClick="onGetInfo"
+            android:text="获取M3U8信息" />
+
+        <Button
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:onClick="onLiveDownload"
+            android:text="直播缓存" />
+
+        <Button
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:onClick="onGetLiveCache"
+            android:text="获取到当前时间的缓存" />
+    </LinearLayout>
+
+
+    <TextView
+        android:id="@+id/tv_savepath_tip"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="默认保存在/sdcard/111/******.ts" />
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal">
+
+        <Button
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:onClick="onPlay"
+            android:text="播放" />
+
+        <Button
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:onClick="onDownload"
+            android:text="下载" />
+
+        <Button
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:onClick="onStopTask1"
+            android:text="停止" />
+        <Button
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:onClick="onMergin"
+            android:text="合并" />
+
+        <TextView
+            android:id="@+id/tv_speed1"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="0 kb/s" />
+    </LinearLayout>
+
+    <ScrollView
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:background="#232323">
+
+        <TextView
+            android:id="@+id/tv_console"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:text="console:"
+            android:textColor="#fff" />
+    </ScrollView>
+</LinearLayout>

+ 8 - 0
Demo/src/main/res/layout/activity_main.xml

@@ -67,6 +67,14 @@
                 android:padding="10dp"
                 android:background="@color/colorAccent"
                 android:text="4.音频播放器案例"/>
+            <TextView
+                android:id="@+id/tv_5"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="10dp"
+                android:padding="10dp"
+                android:background="@color/colorAccent"
+                android:text="5.m3u8视频下载"/>
         </LinearLayout>
     </ScrollView>
 

+ 1 - 0
VideoM3u8/.gitignore

@@ -0,0 +1 @@
+/build

+ 25 - 0
VideoM3u8/build.gradle

@@ -0,0 +1,25 @@
+apply plugin: 'com.android.library'
+
+android {
+    compileSdkVersion 29
+    buildToolsVersion '29.0.0'
+    defaultConfig {
+        minSdkVersion 17
+        targetSdkVersion 29
+        versionCode 39
+        versionName "3.0.9"
+    }
+
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+        }
+    }
+}
+
+dependencies {
+    implementation fileTree(dir: 'libs', include: ['*.jar'])
+    implementation 'androidx.appcompat:appcompat:1.2.0'
+    api files('libs/commons-io-2.5.jar')
+}

BIN
VideoM3u8/libs/commons-io-2.5.jar


+ 25 - 0
VideoM3u8/proguard-rules.pro

@@ -0,0 +1,25 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in F:\sdk/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile

+ 16 - 0
VideoM3u8/src/main/AndroidManifest.xml

@@ -0,0 +1,16 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+
+    package="com.yc.m3u8">
+
+    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+
+    <application
+        android:allowBackup="true"
+        android:label="@string/app_name"
+        android:supportsRtl="true">
+
+    </application>
+
+</manifest>

+ 103 - 0
VideoM3u8/src/main/java/com/yc/m3u8/bean/M3u8.java

@@ -0,0 +1,103 @@
+package com.yc.m3u8.bean;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+
+/**
+ * <pre>
+ *     @author yangchong
+ *     blog  : https://github.com/yangchong211
+ *     time  : 2018/11/9
+ *     desc  : M3U8实体类
+ *     revise:
+ * </pre>
+ */
+public class M3u8 {
+
+    private String basepath;
+    private List<M3u8Ts> tsList = new ArrayList<>();
+    private long startTime;//开始时间
+    private long endTime;//结束时间
+    private long startDownloadTime;//开始下载时间
+    private long endDownloadTime;//结束下载时间
+
+    public String getBasepath() {
+        return basepath;
+    }
+
+    public void setBasepath(String basepath) {
+        this.basepath = basepath;
+    }
+
+    public List<M3u8Ts> getTsList() {
+        return tsList;
+    }
+
+    public void setTsList(List<M3u8Ts> tsList) {
+        this.tsList = tsList;
+    }
+
+    public void addTs(M3u8Ts ts) {
+        this.tsList.add(ts);
+    }
+
+    public long getStartDownloadTime() {
+        return startDownloadTime;
+    }
+
+    public void setStartDownloadTime(long startDownloadTime) {
+        this.startDownloadTime = startDownloadTime;
+    }
+
+    public long getEndDownloadTime() {
+        return endDownloadTime;
+    }
+
+    public void setEndDownloadTime(long endDownloadTime) {
+        this.endDownloadTime = endDownloadTime;
+    }
+
+    /**
+     * 获取开始时间
+     *
+     * @return
+     */
+    public long getStartTime() {
+        if (tsList.size()>0) {
+            Collections.sort(tsList);
+            startTime = tsList.get(0).getLongDate();
+            return startTime;
+        }
+        return 0;
+    }
+
+    /**
+     * 获取结束时间(加上了最后一段时间的持续时间)
+     *
+     * @return
+     */
+    public long getEndTime() {
+        if (tsList.size()>0) {
+            M3u8Ts m3U8Ts = tsList.get(tsList.size() - 1);
+            endTime = m3U8Ts.getLongDate() + (long) (m3U8Ts.getSeconds() * 1000);
+            return endTime;
+        }
+        return 0;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("basepath: " + basepath);
+        for (M3u8Ts ts : tsList) {
+            sb.append("\nts_file_name = " + ts);
+        }
+        sb.append("\n\nstartTime = " + startTime);
+        sb.append("\n\nendTime = " + endTime);
+        sb.append("\n\nstartDownloadTime = " + startDownloadTime);
+        sb.append("\n\nendDownloadTime = " + endDownloadTime);
+        return sb.toString();
+    }
+}

+ 72 - 0
VideoM3u8/src/main/java/com/yc/m3u8/bean/M3u8Ts.java

@@ -0,0 +1,72 @@
+package com.yc.m3u8.bean;
+
+import androidx.annotation.NonNull;
+
+/**
+ * <pre>
+ *     @author yangchong
+ *     blog  : https://github.com/yangchong211
+ *     time  : 2018/11/9
+ *     desc  : m3u8切片类
+ *     revise:
+ * </pre>
+ */
+public class M3u8Ts implements Comparable<M3u8Ts> {
+
+    private String file;
+    private float seconds;
+
+    public M3u8Ts(String file, float seconds) {
+        this.file = file;
+        this.seconds = seconds;
+    }
+
+    public String getFile() {
+        return file;
+    }
+
+    /**
+     * 获取文件名字,支取***.ts
+     * @return
+     */
+    public String getFileName() {
+        String fileName = file.substring(file.lastIndexOf("/") + 1);
+        if (fileName.contains("?")) {
+            return fileName.substring(0, fileName.indexOf("?"));
+        }
+        return fileName;
+    }
+
+    public void setFile(String file) {
+        this.file = file;
+    }
+
+    public float getSeconds() {
+        return seconds;
+    }
+
+    public void setSeconds(float seconds) {
+        this.seconds = seconds;
+    }
+
+    @Override
+    public String toString() {
+        return file + " (" + seconds + "sec)";
+    }
+
+    /**
+     * 获取时间
+     */
+    public long getLongDate() {
+        try {
+            return Long.parseLong(file.substring(0, file.lastIndexOf(".")));
+        } catch (Exception e) {
+            return 0;
+        }
+    }
+
+    @Override
+    public int compareTo(@NonNull M3u8Ts o) {
+        return file.compareTo(o.file);
+    }
+}

+ 24 - 0
VideoM3u8/src/main/java/com/yc/m3u8/inter/BaseListener.java

@@ -0,0 +1,24 @@
+package com.yc.m3u8.inter;
+
+
+/**
+ * <pre>
+ *     @author yangchong
+ *     blog  : https://github.com/yangchong211
+ *     time  : 2018/11/9
+ *     desc  : 监听基类
+ *     revise:
+ * </pre>
+ */
+public interface BaseListener {
+    /**
+     * 开始的时候回调
+     */
+    void onStart();
+
+    /**
+     * 错误的时候回调
+     * @param errorMsg              错误异常
+     */
+    void onError(Throwable errorMsg);
+}

+ 31 - 0
VideoM3u8/src/main/java/com/yc/m3u8/inter/DownLoadListener.java

@@ -0,0 +1,31 @@
+package com.yc.m3u8.inter;
+
+
+
+/**
+ * <pre>
+ *     @author yangchong
+ *     blog  : https://github.com/yangchong211
+ *     time  : 2018/11/9
+ *     desc  : 加载监听器
+ *     revise:
+ * </pre>
+ */
+public interface DownLoadListener {
+    /**
+     * 开始的时候回调
+     */
+    void onStart();
+
+    /**
+     * 错误的时候回调
+     *
+     * @param errorMsg
+     */
+    void onError(Throwable errorMsg);
+
+    /**
+     * 下载完成的时候回调
+     */
+    void onCompleted();
+}

+ 26 - 0
VideoM3u8/src/main/java/com/yc/m3u8/inter/M3U8Listener.java

@@ -0,0 +1,26 @@
+package com.yc.m3u8.inter;
+
+import com.yc.m3u8.bean.M3u8;
+
+/**
+ * @deprecated v2版本过时了,请用
+ * 监听器
+ * Created by HDL on 2017/7/25.
+ */
+
+public abstract class M3U8Listener implements DownLoadListener {
+    public void onM3U8Info(M3u8 m3U8) {
+    }
+
+    public void onDownloadingProgress(int total, int progress) {
+    }
+
+    /**
+     * 当获取到单个文件大小的时候回调
+     *
+     * @param fileSize 单个文件大小
+     */
+    public void onLoadFileSizeForItem(long fileSize) {
+    }
+
+}

+ 37 - 0
VideoM3u8/src/main/java/com/yc/m3u8/inter/OnDownloadListener.java

@@ -0,0 +1,37 @@
+package com.yc.m3u8.inter;
+
+import com.yc.m3u8.inter.BaseListener;
+
+
+/**
+ * <pre>
+ *     @author yangchong
+ *     blog  : https://github.com/yangchong211
+ *     time  : 2018/11/9
+ *     desc  : 下载监听
+ *     revise:
+ * </pre>
+ */
+public interface OnDownloadListener extends BaseListener {
+    /**
+     * 下载m3u8文件.
+     * 注意:这个方法是异步的(子线程中执行),所以不能在此方法中回调,其他方法为主线程中回调
+     *
+     * @param itemFileSize 单个文件的大小
+     * @param totalTs      ts总数
+     * @param curTs        当前下载完成的ts个数
+     */
+    void onDownloading(long itemFileSize, int totalTs, int curTs);
+
+    /**
+     * 下载成功
+     */
+    void onSuccess();
+
+    /**
+     * 当前已经下载的文件大小
+     *
+     * @param curLength
+     */
+    void onProgress(long curLength);
+}

+ 20 - 0
VideoM3u8/src/main/java/com/yc/m3u8/inter/OnM3u8InfoListener.java

@@ -0,0 +1,20 @@
+package com.yc.m3u8.inter;
+
+import com.yc.m3u8.bean.M3u8;
+
+/**
+ * <pre>
+ *     @author yangchong
+ *     blog  : https://github.com/yangchong211
+ *     time  : 2018/11/9
+ *     desc  : 获取M3U8信息
+ *     revise:
+ * </pre>
+ */
+public interface OnM3u8InfoListener extends BaseListener {
+
+    /**
+     * 获取成功的时候回调
+     */
+    void onSuccess(M3u8 m3U8);
+}

+ 106 - 0
VideoM3u8/src/main/java/com/yc/m3u8/manager/M3u8InfoManger.java

@@ -0,0 +1,106 @@
+package com.yc.m3u8.manager;
+
+import android.annotation.SuppressLint;
+import android.os.Handler;
+import android.os.Message;
+
+import com.yc.m3u8.bean.M3u8;
+import com.yc.m3u8.inter.OnM3u8InfoListener;
+import com.yc.m3u8.utils.M3u8FileUtils;
+
+import java.io.IOException;
+
+
+/**
+ * <pre>
+ *     @author yangchong
+ *     blog  : https://github.com/yangchong211
+ *     time  : 2018/11/9
+ *     desc  : 获取M3U8信息的管理器
+ *     revise:
+ * </pre>
+ */
+public class M3u8InfoManger {
+
+    private static M3u8InfoManger mM3U8InfoManger;
+    private OnM3u8InfoListener onM3U8InfoListener;
+    private static final int WHAT_ON_ERROR = 1101;
+    private static final int WHAT_ON_SUCCESS = 1102;
+
+    @SuppressLint("HandlerLeak")
+    private Handler mHandler = new Handler() {
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case WHAT_ON_ERROR:
+                    onM3U8InfoListener.onError((Throwable) msg.obj);
+                    break;
+                case WHAT_ON_SUCCESS:
+                    onM3U8InfoListener.onSuccess((M3u8) msg.obj);
+                    break;
+            }
+        }
+    };
+
+    private M3u8InfoManger() {
+
+    }
+
+    public static M3u8InfoManger getInstance() {
+        synchronized (M3u8InfoManger.class) {
+            if (mM3U8InfoManger == null) {
+                mM3U8InfoManger = new M3u8InfoManger();
+            }
+        }
+        return mM3U8InfoManger;
+    }
+
+    /**
+     * 获取m3u8信息
+     *
+     * @param url
+     * @param onM3U8InfoListener
+     */
+    public synchronized void getM3U8Info(final String url, OnM3u8InfoListener onM3U8InfoListener) {
+        this.onM3U8InfoListener = onM3U8InfoListener;
+        onM3U8InfoListener.onStart();
+        new Thread() {
+            @Override
+            public void run() {
+                try {
+//                    Log.e("hdltag", "run(M3U8InfoManger.java:62):" + url);
+                    M3u8 m3u8 = M3u8FileUtils.parseIndex(url);
+                    handlerSuccess(m3u8);
+                } catch (IOException e) {
+//                    e.printStackTrace();
+                    handlerError(e);
+                }
+            }
+        }.start();
+
+    }
+
+    /**
+     * 通知异常
+     *
+     * @param e
+     */
+    private void handlerError(Throwable e) {
+        Message msg = mHandler.obtainMessage();
+        msg.obj = e;
+        msg.what = WHAT_ON_ERROR;
+        mHandler.sendMessage(msg);
+    }
+
+    /**
+     * 通知成功
+     *
+     * @param m3u8
+     */
+    private void handlerSuccess(M3u8 m3u8) {
+        Message msg = mHandler.obtainMessage();
+        msg.obj = m3u8;
+        msg.what = WHAT_ON_SUCCESS;
+        mHandler.sendMessage(msg);
+    }
+}

+ 363 - 0
VideoM3u8/src/main/java/com/yc/m3u8/manager/M3u8LiveManger.java

@@ -0,0 +1,363 @@
+package com.yc.m3u8.manager;
+
+import android.os.Handler;
+import android.os.Message;
+import android.util.Log;
+
+import com.yc.m3u8.bean.M3u8;
+import com.yc.m3u8.bean.M3u8Ts;
+import com.yc.m3u8.inter.OnDownloadListener;
+import com.yc.m3u8.utils.M3u8FileUtils;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * <pre>
+ *     @author yangchong
+ *     blog  : https://github.com/yangchong211
+ *     time  : 2018/11/9
+ *     desc  : 直播管理器
+ *     revise:
+ * </pre>
+ */
+public class M3u8LiveManger {
+
+    private static M3u8LiveManger mM3U8LiveManger;
+    private ExecutorService executor = Executors.newFixedThreadPool(5);
+    private ExecutorService downloadExecutor = Executors.newFixedThreadPool(5);
+    private Timer getM3U8InfoTimer;
+    private String basePath;
+    private OnDownloadListener onDownloadListener;
+    /**
+     * 当前已经在下完成的大小
+     */
+    private long curLenght = 0;
+    /**
+     * 读取超时时间
+     */
+    private int readTimeout = 30 * 60 * 1000;
+    //当前下载完成的文件个数
+    private static int curTs = 0;
+    //总文件的个数
+    private static int totalTs = 0;
+    //单个文件的大小
+    private static long itemFileSize = 0;
+    /**
+     * 链接超时时间
+     */
+    private int connTimeout = 10 * 1000;
+    private static final int WHAT_ON_ERROR = 1001;
+    private static final int WHAT_ON_PROGRESS = 1002;
+    private static final int WHAT_ON_SUCCESS = 1003;
+    private Handler mHandler = new Handler() {
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case WHAT_ON_ERROR:
+                    onDownloadListener.onError((Throwable) msg.obj);
+                    break;
+                case WHAT_ON_PROGRESS:
+                    onDownloadListener.onDownloading(itemFileSize, totalTs, curTs);
+                    break;
+                case WHAT_ON_SUCCESS:
+                    if (netSpeedTimer != null) {
+                        netSpeedTimer.cancel();
+                    }
+                    onDownloadListener.onSuccess();
+                    break;
+            }
+        }
+    };
+    /**
+     * 定时任务
+     */
+    private Timer netSpeedTimer;
+    /**
+     * 文件保存的目录
+     */
+    private String tempDir = "/sdcard/111/" + System.currentTimeMillis();
+    /**
+     * 已经下载的文件列表
+     */
+    private List<File> downloadedFileList = new ArrayList<>();
+
+    private M3u8LiveManger() {
+    }
+
+    public static M3u8LiveManger getInstance() {
+        if (mM3U8LiveManger == null) {
+            synchronized (M3u8LiveManger.class) {
+                if (mM3U8LiveManger == null) {
+                    mM3U8LiveManger = new M3u8LiveManger();
+                }
+            }
+        }
+        return mM3U8LiveManger;
+    }
+
+    /**
+     * 获取文件临时保存的目录
+     *
+     * @return
+     */
+    public String getTempDir() {
+        return tempDir;
+    }
+
+    /**
+     * 设置文件临时保存的目录
+     *
+     * @param tempDir
+     */
+    public M3u8LiveManger setTempDir(String tempDir) {
+        this.tempDir = tempDir;
+        return this;
+    }
+
+    /**
+     * 缓存视频中
+     *
+     * @param url
+     * @param onDownloadListener1
+     */
+    public void caching(String url, OnDownloadListener onDownloadListener1) {
+        this.onDownloadListener = onDownloadListener1;
+        onDownloadListener.onStart();
+        netSpeedTimer = new Timer();
+        netSpeedTimer.schedule(new TimerTask() {
+            @Override
+            public void run() {
+                onDownloadListener.onProgress(curLenght);
+            }
+        }, 0, 1000);
+        startUpdateM3U8Info(url);
+    }
+
+    /**
+     * 开始下载
+     */
+    private void startDownloadM3U8() {
+        final File dir = new File(tempDir);
+        if (!dir.exists()) {
+            dir.mkdirs();
+        }
+        for (final M3u8Ts m3U8Ts : m3U8TsList) {
+            downloadExecutor.execute(new Runnable() {
+                @Override
+                public void run() {
+                    File file = new File(dir, m3U8Ts.getFileName());
+                    if (!file.exists()) {
+                        FileOutputStream fos = null;
+                        InputStream inputStream = null;
+                        try {
+                            Log.i("hdltag", "run(M3U8DownloadTask.java:278):" + m3U8Ts.getFile());
+                            String urlPath;
+                            if ("http".equals(m3U8Ts.getFile().substring(0, 4))) {
+                                urlPath = m3U8Ts.getFile();
+                            } else {
+                                urlPath = basePath + m3U8Ts.getFile();
+                            }
+                            URL url = new URL(urlPath);
+
+                            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+                            conn.setConnectTimeout(connTimeout);
+                            conn.setReadTimeout(readTimeout);
+                            if (conn.getResponseCode() == 200) {
+                                inputStream = conn.getInputStream();
+                                fos = new FileOutputStream(file);//会自动创建文件
+                                int len = 0;
+                                byte[] buf = new byte[8 * 1024 * 1024];
+                                while ((len = inputStream.read(buf)) != -1) {
+                                    curLenght += len;
+                                    fos.write(buf, 0, len);//写入流中
+                                }
+                                downloadedFileList.add(file);
+                                Log.i("hdltag", "run(M3U8LiveManger.java:138):下完一个了" + file.getAbsolutePath());
+//                                Log.e("hdltag", "run(M3U8DownloadTask.java:188):进度\t" + totalTs + "-----" + curTs);
+                            } else {
+                                handlerError(new Throwable(String.valueOf(conn.getResponseCode())));
+                            }
+                        } catch (MalformedURLException e) {
+//                            e.printStackTrace();
+                            handlerError(e);
+                        } catch (IOException e) {
+//                            e.printStackTrace();
+                            handlerError(e);
+                        } finally {//关流
+                            if (inputStream != null) {
+                                try {
+                                    inputStream.close();
+                                } catch (IOException e) {
+//                                    e.printStackTrace();
+                                }
+                            }
+                            if (fos != null) {
+                                try {
+                                    fos.close();
+                                } catch (IOException e) {
+//                                    e.printStackTrace();
+                                }
+                            }
+                        }
+                        curTs++;
+                        if (curTs == 3) {
+                            itemFileSize = file.length();
+                        }
+                        mHandler.sendEmptyMessage(WHAT_ON_PROGRESS);
+                    }
+                }
+            });
+
+        }
+    }
+
+    /**
+     * 通知异常
+     *
+     * @param e
+     */
+    private void handlerError(Throwable e) {
+        if (!"Task running".equals(e.getMessage())) {
+            stop();
+        }
+        //不提示被中断的情况
+        if ("thread interrupted".equals(e.getMessage())) {
+            return;
+        }
+        Message msg = mHandler.obtainMessage();
+        msg.obj = e;
+        msg.what = WHAT_ON_ERROR;
+        mHandler.sendMessage(msg);
+    }
+
+    /**
+     * 请求到的所有下载列表
+     */
+    private List<M3u8Ts> m3U8TsList = new ArrayList<>();
+
+    /**
+     * 定时获取m3u8信息
+     *
+     * @param url
+     */
+    private void startUpdateM3U8Info(final String url) {
+        if (getM3U8InfoTimer != null) {
+            getM3U8InfoTimer.cancel();
+            getM3U8InfoTimer = null;
+        }
+        getM3U8InfoTimer = new Timer();
+        getM3U8InfoTimer.schedule(new TimerTask() {
+            @Override
+            public void run() {
+                executor.execute(new Runnable() {
+                    @Override
+                    public void run() {
+                        try {
+                            M3u8 m3u8 = M3u8FileUtils.parseIndex(url);
+                            if (m3u8 != null && m3u8.getTsList().size() > 0) {
+                                basePath = m3u8.getBasepath();
+                                addTs(m3u8.getTsList());
+//                                m3U8TsList.addAll(m3u8.getTsList());
+                            }
+//                            Log.e("hdltag", "run(M3U8LiveManger.java:59):" + m3u8);
+                        } catch (IOException e) {
+                            e.printStackTrace();
+                            handlerError(e);
+                        }
+                    }
+                });
+            }
+        }, 0, 2000);
+    }
+
+    /**
+     * 添加到下载列表,做去重处理
+     *
+     * @param tsList
+     */
+    private synchronized void addTs(List<M3u8Ts> tsList) {
+        List<M3u8Ts> tempTsList = new ArrayList<>();
+        for (M3u8Ts m3U8Ts : tsList) {
+            boolean isExisted = false;
+            for (M3u8Ts mTs : m3U8TsList) {
+                if (mTs.getFile().equals(m3U8Ts.getFile())) {
+                    isExisted = true;
+                    break;
+                }
+            }
+            if (!isExisted) {
+                tempTsList.add(m3U8Ts);
+            }
+        }
+        if (tempTsList.size() > 0) {
+            m3U8TsList.addAll(tempTsList);
+        }
+        Log.i("hdltag", "addTs(M3U8LiveManger.java:98):有几个了 ---->" + m3U8TsList.size());
+        for (M3u8Ts m3U8Ts : m3U8TsList) {
+            Log.i("hdltag", "addTs(M3U8LiveManger.java:101):" + m3U8Ts.getFile());
+        }
+        //有更新,通知下载
+        startDownloadM3U8();
+    }
+
+    /**
+     * 停止任务
+     */
+    public void stop() {
+        Log.i("hdltag", "stop(M3U8LiveManger.java:106):调用停止了");
+        if (getM3U8InfoTimer != null) {
+            getM3U8InfoTimer.cancel();
+            getM3U8InfoTimer = null;
+        }
+        if (executor != null) {
+            if (!executor.isShutdown()) {
+                executor.shutdownNow();
+            }
+        }
+    }
+
+    /**
+     * 文件保存的目录(包含文件名字,必须是ts结尾)
+     */
+    private String saveFilePath = "/sdcard/11/" + System.currentTimeMillis() + ".ts";
+
+    public String getSaveFilePath() {
+        return saveFilePath;
+    }
+
+    /**
+     * 设置需要将缓存文件保存的位置(包含文件名字,必须是ts结尾)
+     *
+     * @param saveFile
+     */
+    public M3u8LiveManger setSaveFile(String saveFile) {
+        this.saveFilePath = saveFile;
+        return this;
+    }
+
+    /**
+     * 获取从开始到现在的视频
+     */
+    public String getCurrentTs() {
+        try {
+            M3u8FileUtils.merge(downloadedFileList, saveFilePath);
+        } catch (IOException e) {
+            e.printStackTrace();
+            return "";
+        }
+        Log.i("hdltag", "getCurrentTs(M3U8LiveManger.java:287):已保存至 " + saveFilePath);
+        return saveFilePath;
+    }
+}

+ 385 - 0
VideoM3u8/src/main/java/com/yc/m3u8/manager/M3u8Manger.java

@@ -0,0 +1,385 @@
+package com.yc.m3u8.manager;
+
+import android.os.Environment;
+import android.os.Handler;
+import android.os.Message;
+import android.util.Log;
+
+import com.yc.m3u8.bean.M3u8;
+import com.yc.m3u8.inter.M3U8Listener;
+import com.yc.m3u8.bean.M3u8Ts;
+import com.yc.m3u8.task.M3u8DownloadTask;
+import com.yc.m3u8.utils.M3u8FileUtils;
+
+import org.apache.commons.io.IOUtils;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.net.URL;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+
+/**
+ * <pre>
+ *     @author yangchong
+ *     blog  : https://github.com/yangchong211
+ *     time  : 2018/11/9
+ *     desc  : M3u8管理器
+ *     revise: v2过时了,获取M3U8信息{@link M3u8InfoManger}和下载{@link M3u8DownloadTask}隔离出来
+ * </pre>
+ */
+public class M3u8Manger {
+    private static int currDownloadTsCount = 0;//当前下载ts切片的个数
+    private static final int WHAT_ON_START = 166;
+    private static final int WHAT_ON_ERROR = 711;
+    private static final int WHAT_ON_GETINFO = 840;
+    private static final int WHAT_ON_COMPLITED = 625;
+    private static final int WHAT_ON_PROGRESS = 280;
+    private static final int WHAT_ON_FILESIZE_ITEM = 281;
+    private static final String KEY_DEFAULT_TEMP_DIR = "/sdcard/1m3u8temp/";
+    private static M3u8Manger mM3U8Manger;
+    private String url;//m3u8的路径
+    private String saveFilePath = "/sdcard/Movie/" + System.currentTimeMillis() + ".ts";//文件保存路径
+    private String tempDir = KEY_DEFAULT_TEMP_DIR;//m3u8临时文件夹
+    private ExecutorService executor;//10个线程池
+    private M3U8Listener downLoadListener;
+    private boolean isRunning = false;//任务是否正在运行
+    private Handler mHandler = new Handler() {
+        @Override
+        public void handleMessage(Message msg) {
+            if (downLoadListener != null) {
+                switch (msg.what) {
+                    case WHAT_ON_START:
+                        downLoadListener.onStart();
+                        break;
+                    case WHAT_ON_ERROR:
+                        isRunning = false;//停止任务
+                        currDownloadTsCount = 0;//出错也要复位
+                        M3u8FileUtils.clearDir(new File(tempDir));
+                        downLoadListener.onError((Throwable) msg.obj);
+                        break;
+                    case WHAT_ON_GETINFO:
+                        M3u8 m3U8 = (M3u8) msg.obj;
+                        downLoadListener.onM3U8Info(m3U8);
+                        break;
+                    case WHAT_ON_COMPLITED:
+                        currDownloadTsCount = 0;//完成之后要复位
+                        downLoadListener.onCompleted();
+                        break;
+                    case WHAT_ON_FILESIZE_ITEM:
+                        downLoadListener.onLoadFileSizeForItem((Long) msg.obj);
+                        break;
+                    case WHAT_ON_PROGRESS:
+//                        long size = (long) msg.obj;
+                        long curTime = System.currentTimeMillis();
+                        lastTime = curTime;
+                        downLoadListener.onDownloadingProgress(msg.arg1, msg.arg2);
+                        break;
+                }
+            }
+        }
+    };
+    private long lastTime = 0;
+
+    private M3u8Manger() {
+    }
+
+    public static M3u8Manger getInstance() {
+        synchronized (M3u8Manger.class) {
+            if (mM3U8Manger == null) {
+                mM3U8Manger = new M3u8Manger();
+            }
+        }
+        return mM3U8Manger;
+    }
+
+    /**
+     * 下载
+     *
+     * @param downLoadListener
+     */
+    public synchronized void download(M3U8Listener downLoadListener) {
+        this.downLoadListener = downLoadListener;
+        if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
+            if (!isRunning) {
+                startDownload(-1, -1);
+            } else {
+                handlerError(new Throwable("Task isRunning"));
+            }
+        } else {//没有找到sdcard
+            handlerError(new Throwable("SDcard not found"));
+        }
+    }
+
+    /**
+     * 返回时候正在运行中
+     *
+     * @return
+     */
+    public synchronized boolean isRunning() {
+        return isRunning;
+    }
+
+    /**
+     * 下载指定时间的ts
+     *
+     * @param downLoadListener
+     */
+    public synchronized void download(long startDwonloadTime, long endDownloadTime, M3U8Listener downLoadListener) {
+        this.downLoadListener = downLoadListener;
+        if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
+            if (!isRunning) {
+                startDownload(startDwonloadTime, endDownloadTime);
+            } else {
+                handlerError(new Throwable("Task isRunning"));
+            }
+        } else {//没有找到sdcard
+            handlerError(new Throwable("SDcard not found"));
+        }
+    }
+
+    /**
+     * 停止任务
+     */
+    public synchronized void stop() {
+        isRunning = false;
+        if (executor != null) {
+            executor.shutdownNow();
+            executor = null;
+            //清空临时目录
+            M3u8FileUtils.clearDir(new File(tempDir));
+        }
+//        mHandler.sendEmptyMessage(WHAT_ON_COMPLITED);
+    }
+
+    /**
+     * 获取m3u8
+     *
+     * @param downLoadListener
+     */
+    public synchronized void getM3U8(M3U8Listener downLoadListener) {
+        this.downLoadListener = downLoadListener;
+        downLoadListener.onStart();//开始了
+        if (!isRunning) {
+            new Thread() {
+                @Override
+                public void run() {
+                    isRunning = true;
+                    try {
+                        M3u8 m3u8 = M3u8FileUtils.parseIndex(url);
+                        isRunning = false;//获取成功之后要复位
+                        sendM3u8Info(m3u8);
+                        mHandler.sendEmptyMessage(WHAT_ON_COMPLITED);
+                    } catch (IOException e) {
+                        e.printStackTrace();
+                        handlerError(e);
+                    }
+                }
+            }.start();
+        } else {
+            handlerError(new Throwable("Task isRunning"));
+        }
+
+    }
+
+    /**
+     * 开始下载了
+     */
+    private synchronized void startDownload(final long startDwonloadTime, final long endDownloadTime) {
+        mHandler.sendEmptyMessage(WHAT_ON_START);
+        isRunning = true;//开始下载了
+        new Thread() {
+            @Override
+            public void run() {
+                try {
+                    M3u8 m3u8 = null;
+                    try {
+                        m3u8 = M3u8FileUtils.parseIndex(url);
+                        m3u8.setStartDownloadTime(startDwonloadTime);
+                        m3u8.setEndDownloadTime(endDownloadTime);
+                        sendM3u8Info(m3u8);
+                    } catch (Exception e) {
+                        handlerError(e);
+                        return;
+                    }
+                    if (executor != null && executor.isTerminated()) {
+                        executor.shutdownNow();
+                        executor = null;
+                    }
+                    executor = Executors.newFixedThreadPool(10);
+                    if (isRunning()) {
+                        download(m3u8, tempDir);//开始下载,保存在临时文件中
+                    }
+                    if (executor != null) {
+                        executor.shutdown();//下载完成之后要关闭线程池
+                    }
+//                    System.out.println("Wait for downloader...");
+                    while (executor != null && !executor.isTerminated()) {
+                        Thread.sleep(100);
+                    }
+                    if (isRunning()) {
+                        String tempFile = tempDir + "/" + System.currentTimeMillis() + ".ts";
+                        M3u8FileUtils.merge(m3u8, tempFile);//合并ts
+                        //移动到指定的目录
+                        M3u8FileUtils.moveFile(tempFile, saveFilePath);
+                        mHandler.sendEmptyMessage(WHAT_ON_COMPLITED);
+                        isRunning = false;//复位
+                    }
+                } catch (IOException e) {
+                    e.printStackTrace();
+                    handlerError(e);
+                } catch (InterruptedException e) {
+                    e.printStackTrace();
+                    handlerError(e);
+                } finally {
+                    //清空临时目录
+                    M3u8FileUtils.clearDir(new File(tempDir));
+                }
+            }
+        }.start();
+    }
+
+    /**
+     * 通知拿到消息
+     *
+     * @param m3u8
+     */
+    private void sendM3u8Info(M3u8 m3u8) {
+        Message msg = mHandler.obtainMessage();
+        msg.obj = m3u8;
+        msg.what = WHAT_ON_GETINFO;
+        mHandler.sendMessage(msg);
+    }
+
+    /**
+     * 通知异常
+     *
+     * @param e
+     */
+    private void handlerError(Throwable e) {
+        Message msg = mHandler.obtainMessage();
+        msg.obj = e;
+        msg.what = WHAT_ON_ERROR;
+        mHandler.sendMessage(msg);
+    }
+
+    /**
+     * 设置m3u8文件的路径
+     *
+     * @param url
+     * @return
+     */
+    public synchronized M3u8Manger setUrl(String url) {
+        this.url = url;
+        return this;
+    }
+
+    /**
+     * 设置保存文件的名字
+     *
+     * @param saveFilePath
+     * @return
+     */
+    public synchronized M3u8Manger setSaveFilePath(String saveFilePath) {
+        this.saveFilePath = saveFilePath;
+        tempDir = KEY_DEFAULT_TEMP_DIR;
+//        tempDir += new File(saveFilePath).getName();
+        return this;
+    }
+
+    /**
+     * 下载
+     *
+     * @param m3u8
+     * @param saveFileName
+     * @throws IOException
+     */
+    private void download(final M3u8 m3u8, final String saveFileName) throws IOException {
+        Log.e("hdltag", "caching(M3U8Manger.java:293):" + saveFileName);
+        final File dir = new File(saveFileName);
+        if (!dir.exists()) {
+            dir.mkdirs();
+        } else if (dir.list().length > 0) {//保存的路径必须必须为空或者文件夹不存在
+            M3u8FileUtils.clearDir(dir);//清空文件
+        }
+        final List<M3u8Ts> downList = M3u8FileUtils.getLimitM3U8Ts(m3u8);
+        final int total = downList.size();
+
+        for (final M3u8Ts ts : downList) {
+            if (executor != null && !executor.isShutdown()) {//正常的时候才能走
+                executor.execute(new Runnable() {
+                    @Override
+                    public void run() {
+                        try {
+//                        System.out.println("caching " + (m3u8.getTsList().indexOf(ts) + 1) + "/"
+//                                + m3u8.getTsList().size() + ": " + ts);
+                            if (isRunning()) {
+                                FileOutputStream writer = null;
+                                long size = 0;
+                                try {
+                                    writer = new FileOutputStream(new File(dir, ts.getFileName()));
+                                    size = IOUtils.copyLarge(new URL(m3u8.getBasepath() + ts.getFileName()).openStream(), writer);
+                                } catch (InterruptedIOException exception) {
+                                    isRunning = false;
+                                    currDownloadTsCount = 0;
+                                    System.out.println("----------InterruptedIOException------------");
+                                    return;
+                                } finally {
+                                    if (writer != null) {
+                                        writer.close();
+                                    }
+                                }
+                                currDownloadTsCount++;
+                                if (currDownloadTsCount == 2) {//由于每个ts文件的大小基本是固定的(头尾有点差距),可以通过单个文件的大小来算整个文件的大小
+                                    long length = new File(dir, ts.getFileName()).length();
+                                    Message msg = mHandler.obtainMessage();
+                                    msg.what = WHAT_ON_FILESIZE_ITEM;
+                                    msg.obj = length;
+                                    mHandler.sendMessage(msg);
+                                }
+                                Message msg = mHandler.obtainMessage();
+                                msg.what = WHAT_ON_PROGRESS;
+                                msg.obj = size;
+                                msg.arg1 = total;
+                                msg.arg2 = currDownloadTsCount;
+                                mHandler.sendMessage(msg);
+                            }
+//                        System.out.println("caching ok for: " + ts);
+                        } catch (IOException e) {
+                            e.printStackTrace();
+                            handlerError(e);
+                        }
+                    }
+                });
+            } else {
+                handlerError(new Throwable("executor is shutdown"));
+            }
+        }
+
+    }
+
+    /**
+     * 获取当前下载速度
+     *
+     * @return
+     */
+    public String getNetSpeed() {
+        int speed = (int) (Math.random() * 1024 + 1);
+        return speed + " kb/s";
+    }
+
+    /**
+     * 获取当前下载速度
+     *
+     * @param max 最大值
+     * @return
+     */
+    public String getNetSpeed(int max) {
+        int speed = (int) (Math.random() * max + 1);
+        return speed + " kb/s";
+    }
+}

+ 389 - 0
VideoM3u8/src/main/java/com/yc/m3u8/task/M3u8DownloadTask.java

@@ -0,0 +1,389 @@
+package com.yc.m3u8.task;
+
+import android.os.Environment;
+import android.os.Handler;
+import android.os.Message;
+
+import com.yc.m3u8.manager.M3u8InfoManger;
+import com.yc.m3u8.bean.M3u8;
+import com.yc.m3u8.bean.M3u8Ts;
+import com.yc.m3u8.inter.OnDownloadListener;
+import com.yc.m3u8.inter.OnM3u8InfoListener;
+import com.yc.m3u8.utils.M3u8FileUtils;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InterruptedIOException;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * <pre>
+ *     @author yangchong
+ *     blog  : https://github.com/yangchong211
+ *     time  : 2018/11/9
+ *     desc  : M3U8下载管理器
+ *     revise:
+ * </pre>
+ */
+public class M3u8DownloadTask {
+    private OnDownloadListener onDownloadListener;
+    private static final int WHAT_ON_ERROR = 1001;
+    private static final int WHAT_ON_PROGRESS = 1002;
+    private static final int WHAT_ON_SUCCESS = 1003;
+
+    public M3u8DownloadTask(String taskId) {
+        this.taskId = taskId;
+        //需要加上当前时间作为文件夹(由于合并时是根据文件夹来合并的,合并之后需要删除所有的ts文件,这里用到了多线程,所以需要按文件夹来存ts)
+        tempDir += File.separator + System.currentTimeMillis() / (1000 * 60 * 60 * 24) + "-" + taskId;
+    }
+
+    //临时下载目录
+    private String tempDir = Environment.getExternalStorageDirectory().getPath() + File.separator + "m3u8temp";
+    //最终文件保存的路径
+    private String saveFilePath = Environment.getExternalStorageDirectory().getPath() + File.separator + "11m3u8";
+    //当前下载完成的文件个数
+    private static int curTs = 0;
+    //总文件的个数
+    private static int totalTs = 0;
+    //单个文件的大小
+    private static long itemFileSize = 0;
+    /**
+     * 当前已经在下完成的大小
+     */
+    private long curLenght = 0;
+    /**
+     * 任务是否正在运行中
+     */
+    private boolean isRunning = false;
+    /**
+     * 任务id,用于断点续传.
+     * 如果任务已经停止、下一次会根据此id来找到上一次已经下载完成的ts文件,开始下载之前,会判断是否已经下载过了,下载了就不再下载
+     */
+    private String taskId = "0";
+    /**
+     * 线程池最大线程数,默认为3
+     */
+    private int threadCount = 3;
+    /**
+     * 时候清楚临时目录,默认清除
+     */
+    private boolean isClearTempDir = true;
+    /**
+     * 读取超时时间
+     */
+    private int readTimeout = 30 * 60 * 1000;
+    /**
+     * 链接超时时间
+     */
+    private int connTimeout = 10 * 1000;
+    /**
+     * 定时任务
+     */
+    private Timer netSpeedTimer;
+    private ExecutorService executor;//线程池
+    private Handler mHandler = new Handler() {
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case WHAT_ON_ERROR:
+                    onDownloadListener.onError((Throwable) msg.obj);
+                    break;
+                case WHAT_ON_PROGRESS:
+                    onDownloadListener.onDownloading(itemFileSize, totalTs, curTs);
+                    break;
+                case WHAT_ON_SUCCESS:
+                    if (netSpeedTimer != null) {
+                        netSpeedTimer.cancel();
+                    }
+                    onDownloadListener.onSuccess();
+                    break;
+            }
+        }
+    };
+
+    /**
+     * 设置最大线程数
+     *
+     * @param threadCount
+     */
+    public void setThreadCount(int threadCount) {
+        this.threadCount = threadCount;
+    }
+
+    /**
+     * 开始下载
+     *
+     * @param url
+     * @param onDownloadListener
+     */
+    public void download(final String url, OnDownloadListener onDownloadListener) {
+        this.onDownloadListener = onDownloadListener;
+        if (!isRunning()) {
+            getM3U8Info(url);
+        } else {
+            handlerError(new Throwable("Task running"));
+        }
+    }
+
+    public long getReadTimeout() {
+        return readTimeout;
+    }
+
+    public void setReadTimeout(int readTimeout) {
+        this.readTimeout = readTimeout;
+    }
+
+    public long getConnTimeout() {
+        return connTimeout;
+    }
+
+    public void setConnTimeout(int connTimeout) {
+        this.connTimeout = connTimeout;
+    }
+
+    public boolean isClearTempDir() {
+        return isClearTempDir;
+    }
+
+    public void setClearTempDir(boolean clearTempDir) {
+        isClearTempDir = clearTempDir;
+    }
+
+    public String getTaskId() {
+        return taskId;
+    }
+
+    /**
+     * 获取任务是否正在执行
+     *
+     * @return
+     */
+    public boolean isRunning() {
+        return isRunning;
+    }
+
+    /**
+     * 先获取m3u8信息
+     *
+     * @param url
+     */
+    private void getM3U8Info(String url) {
+        M3u8InfoManger.getInstance().getM3U8Info(url, new OnM3u8InfoListener() {
+            @Override
+            public void onSuccess(final M3u8 m3U8) {
+                new Thread() {
+                    @Override
+                    public void run() {
+                        try {
+                            startDownload(m3U8);
+                            if (executor != null) {
+                                executor.shutdown();//下载完成之后要关闭线程池
+                            }
+                            while (executor != null && !executor.isTerminated()) {
+                                //等待中
+                                Thread.sleep(100);
+                            }
+                            if (isRunning) {
+                                String saveFileName = saveFilePath.substring(saveFilePath.lastIndexOf("/") + 1);
+                                String tempSaveFile = tempDir + File.separator + saveFileName;//生成临时文件
+                                M3u8FileUtils.merge(m3U8, tempSaveFile, tempDir);//合并ts
+                                //移动到指定的目录
+                                M3u8FileUtils.moveFile(tempSaveFile, saveFilePath);//移动到指定文件夹
+                                if (isClearTempDir) {
+                                    mHandler.postDelayed(new Runnable() {
+                                        @Override
+                                        public void run() {
+                                            M3u8FileUtils.clearDir(new File(tempDir));//清空一下临时文件
+                                        }
+                                    }, 20 * 1000);//20s之后再删除
+                                }
+                                mHandler.sendEmptyMessage(WHAT_ON_SUCCESS);
+                                isRunning = false;
+                            }
+                        } catch (InterruptedIOException e) {
+//                    e.printStackTrace();
+                            //被中断了,使用stop时会抛出这个,不需要处理
+//                            handlerError(e);
+                            return;
+                        } catch (IOException e) {
+//                    e.printStackTrace();
+                            handlerError(e);
+                            return;
+                        } catch (InterruptedException e) {
+//                            e.printStackTrace();
+                            handlerError(e);
+                        }
+                    }
+                }.start();
+            }
+
+            @Override
+            public void onStart() {
+                onDownloadListener.onStart();
+                isRunning = true;
+            }
+
+            @Override
+            public void onError(Throwable errorMsg) {
+                handlerError(errorMsg);
+            }
+        });
+    }
+
+    /**
+     * 开始下载
+     *
+     * @param m3U8
+     */
+    private void startDownload(final M3u8 m3U8) {
+        if (m3U8 == null) {
+            handlerError(new Throwable("M3U8 is null"));
+            return;
+        }
+        final File dir = new File(tempDir);
+        //没有就创建
+        if (!dir.exists()) {
+            dir.mkdirs();
+        }/* else {
+            //有就清空内容
+            MUtils.clearDir(dir);
+        }*/
+        totalTs = m3U8.getTsList().size();
+        if (executor != null && executor.isTerminated()) {
+            executor.shutdownNow();
+            executor = null;
+        }
+        executor = Executors.newFixedThreadPool(threadCount);
+        final String basePath = m3U8.getBasepath();
+        netSpeedTimer = new Timer();
+        netSpeedTimer.schedule(new TimerTask() {
+            @Override
+            public void run() {
+                onDownloadListener.onProgress(curLenght);
+            }
+        }, 0, 1000);
+        for (final M3u8Ts m3U8Ts : m3U8.getTsList()) {//循环下载
+            executor.execute(new Runnable() {
+                @Override
+                public void run() {
+                    File file = new File(dir + File.separator + m3U8Ts.getFileName());
+                    if (!file.exists()) {//下载过的就不管了
+                        FileOutputStream fos = null;
+                        InputStream inputStream = null;
+                        try {
+                            String urlPath;
+                            if ("http".equals(m3U8Ts.getFile().substring(0, 4))) {
+                                urlPath = m3U8Ts.getFile();
+                            } else {
+                                urlPath = basePath + m3U8Ts.getFile();
+                            }
+                            URL url = new URL(urlPath);
+
+                            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+                            conn.setConnectTimeout(connTimeout);
+                            conn.setReadTimeout(readTimeout);
+                            if (conn.getResponseCode() == 200) {
+                                inputStream = conn.getInputStream();
+                                fos = new FileOutputStream(file);//会自动创建文件
+                                int len = 0;
+                                byte[] buf = new byte[8 * 1024 * 1024];
+                                while ((len = inputStream.read(buf)) != -1) {
+                                    curLenght += len;
+                                    fos.write(buf, 0, len);//写入流中
+                                }
+                            } else {
+                                handlerError(new Throwable(String.valueOf(conn.getResponseCode())));
+                            }
+                        } catch (MalformedURLException e) {
+//                            e.printStackTrace();
+                            handlerError(e);
+                        } catch (IOException e) {
+//                            e.printStackTrace();
+                            handlerError(e);
+                        } finally {//关流
+                            if (inputStream != null) {
+                                try {
+                                    inputStream.close();
+                                } catch (IOException e) {
+//                                    e.printStackTrace();
+                                }
+                            }
+                            if (fos != null) {
+                                try {
+                                    fos.close();
+                                } catch (IOException e) {
+//                                    e.printStackTrace();
+                                }
+                            }
+                        }
+                        curTs++;
+                        if (curTs == 3) {
+                            itemFileSize = file.length();
+                        }
+                        mHandler.sendEmptyMessage(WHAT_ON_PROGRESS);
+                    }
+                }
+            });
+        }
+    }
+
+    public String getSaveFilePath() {
+        return saveFilePath;
+    }
+
+    public void setSaveFilePath(String saveFilePath) {
+        this.saveFilePath = saveFilePath;
+    }
+
+    /**
+     * 通知异常
+     *
+     * @param e
+     */
+    private void handlerError(Throwable e) {
+        if (!"Task running".equals(e.getMessage())) {
+            stop();
+        }
+        //不提示被中断的情况
+        if ("thread interrupted".equals(e.getMessage())) {
+            return;
+        }
+        Message msg = mHandler.obtainMessage();
+        msg.obj = e;
+        msg.what = WHAT_ON_ERROR;
+        mHandler.sendMessage(msg);
+    }
+
+    /**
+     * 停止任务
+     */
+    public void stop() {
+        if (netSpeedTimer != null) {
+            netSpeedTimer.cancel();
+            netSpeedTimer = null;
+        }
+        isRunning = false;
+        if (executor != null) {
+            executor.shutdownNow();
+        }
+    }
+
+//    /**
+//     * 获取当前下载速度
+//     *
+//     * @param max 最大值
+//     * @return
+//     */
+//    public String getNetSpeed(int max) {
+//        int speed = (int) (Math.random() * max + 1);
+//        return speed + " kb/s";
+//    }
+}

+ 205 - 0
VideoM3u8/src/main/java/com/yc/m3u8/utils/M3u8FileUtils.java

@@ -0,0 +1,205 @@
+package com.yc.m3u8.utils;
+
+import android.util.Log;
+
+import com.yc.m3u8.bean.M3u8;
+import com.yc.m3u8.bean.M3u8Ts;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.IOUtils;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+
+
+/**
+ * <pre>
+ *     @author yangchong
+ *     blog  : https://github.com/yangchong211
+ *     time  : 2018/11/9
+ *     desc  : M3u8工具类
+ *     revise:
+ * </pre>
+ */
+public final class M3u8FileUtils {
+
+    /**
+     * 将Url转换为M3U8对象
+     * @param url
+     * @return
+     * @throws IOException
+     */
+    public static M3u8 parseIndex(String url) throws IOException {
+        HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
+        if (conn.getResponseCode() == 200) {
+            String realUrl = conn.getURL().toString();
+            BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
+            String basepath = realUrl.substring(0, realUrl.lastIndexOf("/") + 1);
+            M3u8 ret = new M3u8();
+            ret.setBasepath(basepath);
+            String line;
+            float seconds = 0;
+            while ((line = reader.readLine()) != null) {
+                if (line.startsWith("#")) {
+                    if (line.startsWith("#EXTINF:")) {
+                        line = line.substring(8);
+                        if (line.endsWith(",")) {
+                            line = line.substring(0, line.length() - 1);
+                        }
+                        seconds = Float.parseFloat(line);
+                    }
+                    continue;
+                }
+                if (line.endsWith("m3u8")) {
+                    return parseIndex(basepath + line);
+                }
+                ret.addTs(new M3u8Ts(line, seconds));
+                seconds = 0;
+            }
+            reader.close();
+            return ret;
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * 将M3U8对象的所有ts切片合并为1个
+     *
+     * @param m3u8
+     * @param tofile
+     * @throws IOException
+     */
+    public static String merge(M3u8 m3u8, String tofile) throws IOException {
+        List<M3u8Ts> mergeList = getLimitM3U8Ts(m3u8);
+        File file = new File(tofile);
+        FileOutputStream fos = new FileOutputStream(file);
+        for (M3u8Ts ts : mergeList) {
+            IOUtils.copyLarge(new FileInputStream(new File(file.getParentFile(), ts.getFileName())), fos);
+        }
+        fos.close();
+        return tofile;
+    }
+
+    /**
+     * 合并文件
+     *
+     * @param fileList 文件列表
+     * @param toFile   合并之后的文件
+     */
+    public static void merge(List<File> fileList, String toFile) throws IOException {
+        File file = new File(toFile);
+        File dir=file.getParentFile();
+        if (!dir.exists()) {
+            dir.mkdirs();
+        }
+        FileOutputStream fos = new FileOutputStream(file);
+        for (File tsFile : fileList) {
+            IOUtils.copyLarge(new FileInputStream(tsFile), fos);
+        }
+        fos.close();
+    }
+
+    /**
+     * 将M3U8对象的所有ts切片合并为1个
+     *
+     * @param m3u8
+     * @param tofile
+     * @throws IOException
+     */
+    public static void merge(M3u8 m3u8, String tofile, String basePath) throws IOException {
+        List<M3u8Ts> mergeList = getLimitM3U8Ts(m3u8);
+        File saveFile = new File(tofile);
+        FileOutputStream fos = new FileOutputStream(saveFile);
+        File file;
+        for (M3u8Ts ts : mergeList) {
+            file = new File(basePath, ts.getFileName());
+            if (file.isFile() && file.exists()) {
+                IOUtils.copyLarge(new FileInputStream(file), fos);
+            }
+        }
+        fos.close();
+    }
+
+    /**
+     * 移动文件
+     *
+     * @param sFile
+     * @param tFile
+     */
+    public static void moveFile(String sFile, String tFile) {
+        try {
+            FileUtils.moveFile(new File(sFile), new File(tFile));
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * 清空文件夹
+     */
+    public static void clearDir(File dir) {
+        if (dir.exists()) {// 判断文件是否存在
+            if (dir.isFile()) {// 判断是否是文件
+                dir.delete();// 删除文件
+            } else if (dir.isDirectory()) {// 否则如果它是一个目录
+                File[] files = dir.listFiles();// 声明目录下所有的文件 files[];
+                for (int i = 0; i < files.length; i++) {// 遍历目录下所有的文件
+                    clearDir(files[i]);// 把每个文件用这个方法进行迭代
+                }
+                dir.delete();// 删除文件夹
+            }
+        }
+    }
+
+    /**
+     * 获取指定区间的M3U8切片
+     *
+     * @param m3u8
+     * @return
+     */
+    public static List<M3u8Ts> getLimitM3U8Ts(M3u8 m3u8) {
+        List<M3u8Ts> downList = new ArrayList<>();
+        if (m3u8.getStartDownloadTime() < m3u8.getStartTime()
+                || m3u8.getEndDownloadTime() > m3u8.getEndTime()) {
+            downList = m3u8.getTsList();
+            return downList;
+        }
+        if ((m3u8.getStartDownloadTime() == -1 && m3u8.getEndDownloadTime() == -1)
+                || m3u8.getEndDownloadTime() <= m3u8.getStartDownloadTime()) {
+            downList = m3u8.getTsList();
+        } else if (m3u8.getStartDownloadTime() == -1 && m3u8.getEndDownloadTime() > -1) {
+            for (final M3u8Ts ts : m3u8.getTsList()) {
+                //从头下到指定时间
+                if (ts.getLongDate() <= m3u8.getEndDownloadTime()) {
+                    downList.add(ts);
+                }
+            }
+        } else if (m3u8.getStartDownloadTime() > -1 && m3u8.getEndDownloadTime() == -1) {
+            for (final M3u8Ts ts : m3u8.getTsList()) {
+                //从指定时间下到尾部
+                if (ts.getLongDate() >= m3u8.getStartDownloadTime()) {
+                    downList.add(ts);
+                }
+            }
+        } else {
+            //从指定开始时间下载到指定结束时间
+            for (final M3u8Ts ts : m3u8.getTsList()) {
+                if (m3u8.getStartDownloadTime() <= ts.getLongDate() && ts.getLongDate() <= m3u8.getEndDownloadTime()) {
+                    //指定区间的ts
+                    downList.add(ts);
+                }
+            }
+        }
+        Log.e("hdltag", "getLimitM3U8Ts(MUtils.java:152):" + downList);
+        return downList;
+    }
+}

+ 47 - 0
VideoM3u8/src/main/java/com/yc/m3u8/utils/NetSpeedUtils.java

@@ -0,0 +1,47 @@
+package com.yc.m3u8.utils;
+
+/**
+ * 网速工具
+ * Created by HDL on 2017/8/14.
+ */
+
+public class NetSpeedUtils {
+    private static NetSpeedUtils mNetSpeedUtils;
+
+    private NetSpeedUtils() {
+    }
+
+    public static NetSpeedUtils getInstance() {
+        synchronized (NetSpeedUtils.class) {
+            if (mNetSpeedUtils == null) {
+                mNetSpeedUtils = new NetSpeedUtils();
+            }
+        }
+        return mNetSpeedUtils;
+    }
+
+    //字节大小,K,M,G
+    public static final long KB = 1024;
+    public static final long MB = KB * 1024;
+    public static final long GB = MB * 1024;
+
+    /**
+     * 文件字节大小显示成M,G和K
+     *
+     * @param size
+     * @return
+     */
+    public String displayFileSize(long size) {
+        if (size >= GB) {
+            return String.format("%.1f GB", (float) size / GB);
+        } else if (size >= MB) {
+            float value = (float) size / MB;
+            return String.format(value > 100 ? "%.0f MB" : "%.1f MB", value);
+        } else if (size >= KB) {
+            float value = (float) size / KB;
+            return String.format(value > 100 ? "%.0f KB" : "%.1f KB", value);
+        } else {
+            return String.format("%d B", size);
+        }
+    }
+}

+ 3 - 0
VideoM3u8/src/main/res/values/strings.xml

@@ -0,0 +1,3 @@
+<resources>
+    <string name="app_name">M3U8MangerLib</string>
+</resources>

+ 2 - 0
VideoPlayer/proguard-rules.pro

@@ -19,3 +19,5 @@
 # If you keep the line number information, uncomment this to
 # hide the original source file name.
 #-renamesourcefileattribute SourceFile
+
+

+ 11 - 1
VideoPlayer/src/main/java/com/yc/video/bridge/ControlWrapper.java

@@ -42,7 +42,17 @@ public class ControlWrapper implements InterVideoPlayer, InterVideoController {
         mVideoPlayer = videoPlayer;
         mController = controller;
     }
-    
+
+    @Override
+    public void setUrl(String url) {
+        mVideoPlayer.setUrl(url);
+    }
+
+    @Override
+    public String getUrl() {
+        return mVideoPlayer.getUrl();
+    }
+
     @Override
     public void start() {
         mVideoPlayer.start();

+ 5 - 1
VideoPlayer/src/main/java/com/yc/video/config/ConstantKeys.java

@@ -80,6 +80,7 @@ public final class ConstantKeys {
 
     /**
      * 播放状态,主要是指播放器的各种状态
+     * -4               链接为空
      * -3               解析异常
      * -2               播放错误,网络异常
      * -1               播放错误
@@ -92,9 +93,11 @@ public final class ConstantKeys {
      * 6                暂停缓冲(播放器正在播放时,缓冲区数据不足,进行缓冲,此时暂停播放器,继续缓冲,缓冲区数据足够后恢复暂停
      * 7                播放完成
      * 8                开始播放中止
+     * 9                即将开播
      */
     @Retention(RetentionPolicy.SOURCE)
     public @interface CurrentState{
+        int STATE_URL_NULL = -4;
         int STATE_PARSE_ERROR = -3;
         int STATE_NETWORK_ERROR = -2;
         int STATE_ERROR = -1;
@@ -107,13 +110,14 @@ public final class ConstantKeys {
         int STATE_BUFFERING_PAUSED = 6;
         int STATE_COMPLETED = 7;
         int STATE_START_ABORT = 8;
+        int STATE_ONCE_LIVE = 9;
     }
 
     @IntDef({CurrentState.STATE_ERROR,CurrentState.STATE_IDLE,CurrentState.STATE_PREPARING,
             CurrentState.STATE_PREPARED,CurrentState.STATE_PLAYING,CurrentState.STATE_PAUSED,
             CurrentState.STATE_BUFFERING_PLAYING,CurrentState.STATE_BUFFERING_PAUSED,
             CurrentState.STATE_COMPLETED,CurrentState.STATE_START_ABORT,CurrentState.STATE_NETWORK_ERROR,
-            CurrentState.STATE_PARSE_ERROR})
+            CurrentState.STATE_PARSE_ERROR,CurrentState.STATE_URL_NULL,CurrentState.STATE_ONCE_LIVE})
     @Retention(RetentionPolicy.SOURCE)
     public @interface CurrentStateType{}
 

+ 55 - 0
VideoPlayer/src/main/java/com/yc/video/config/VideoPlayerConfig.java

@@ -51,15 +51,50 @@ public class VideoPlayerConfig {
          * 默认是关闭日志的
          */
         private boolean mIsEnableLog = false;
+        /**
+         * 在移动环境下调用start()后是否继续播放,默认不继续播放
+         */
         private boolean mPlayOnMobileNetwork;
+        /**
+         * 是否监听设备方向来切换全屏/半屏, 默认不开启
+         */
         private boolean mEnableOrientation;
+        /**
+         * 是否开启AudioFocus监听, 默认开启
+         */
         private boolean mEnableAudioFocus = true;
+        /**
+         * 设置进度管理器,用于保存播放进度
+         */
         private ProgressManager mProgressManager;
+        /**
+         * 自定义播放核心
+         */
         private PlayerFactory mPlayerFactory;
+        /**
+         * 自定义视频全局埋点事件
+         */
         private BuriedPointEvent mBuriedPointEvent;
+        /**
+         * 设置视频比例
+         */
         private int mScreenScaleType;
+        /**
+         * 自定义RenderView
+         */
         private SurfaceFactory mRenderViewFactory;
+        /**
+         * 是否适配刘海屏,默认适配
+         */
         private boolean mAdaptCutout = true;
+        /**
+         * 是否设置倒计时n秒吐司
+         */
+        private boolean mIsShowToast = false;
+        /**
+         * 倒计时n秒时间
+         */
+        private long mShowToastTime = 5;
 
         /**
          * 是否监听设备方向来切换全屏/半屏, 默认不开启
@@ -149,6 +184,22 @@ public class VideoPlayerConfig {
             return this;
         }
 
+        /**
+         * 是否设置倒计时n秒吐司
+         */
+        public Builder setIsShowToast(boolean isShowToast) {
+            mIsShowToast = isShowToast;
+            return this;
+        }
+
+        /**
+         * 倒计时n秒时间
+         */
+        public Builder setShowToastTime(long showToastTime) {
+            mShowToastTime = showToastTime;
+            return this;
+        }
+
         public VideoPlayerConfig build() {
             //创建builder对象
             return new VideoPlayerConfig(this);
@@ -166,6 +217,8 @@ public class VideoPlayerConfig {
     public final int mScreenScaleType;
     public final SurfaceFactory mRenderViewFactory;
     public final boolean mAdaptCutout;
+    public final boolean mIsShowToast ;
+    public final long mShowToastTime;
 
     private VideoPlayerConfig(Builder builder) {
         mIsEnableLog = builder.mIsEnableLog;
@@ -192,6 +245,8 @@ public class VideoPlayerConfig {
         if (mContext!=null){
             BaseToast.init(mContext);
         }
+        mIsShowToast = builder.mIsShowToast;
+        mShowToastTime = builder.mShowToastTime;
     }
 
 

+ 3 - 1
VideoPlayer/src/main/java/com/yc/video/controller/BaseVideoController.java

@@ -397,7 +397,9 @@ public abstract class BaseVideoController extends FrameLayout implements InterVi
         public void run() {
             int pos = setProgress();
             if (mControlWrapper.isPlaying()) {
-                postDelayed(this, (long) ((1000  - pos % 1000) / mControlWrapper.getSpeed()));
+                float speed = mControlWrapper.getSpeed();
+                //postDelayed(this, 1000);
+                postDelayed(this, (long) ((1000  - pos % 1000) / speed));
             } else {
                 mIsStartProgress = false;
             }

+ 12 - 0
VideoPlayer/src/main/java/com/yc/video/player/InterVideoPlayer.java

@@ -45,6 +45,18 @@ import com.yc.video.config.ConstantKeys;
  */
 public interface InterVideoPlayer {
 
+    /**
+     * 设置链接
+     * @param url                           url
+     */
+    void setUrl(String url);
+
+    /**
+     * 获取播放链接
+     * @return                              链接
+     */
+    String getUrl();
+
     /**
      * 开始播放
      */

+ 17 - 5
VideoPlayer/src/main/java/com/yc/video/player/VideoPlayer.java

@@ -21,7 +21,6 @@ import android.content.res.TypedArray;
 import android.graphics.Bitmap;
 import android.graphics.Color;
 import android.os.Parcelable;
-
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import android.text.TextUtils;
@@ -33,20 +32,16 @@ import com.yc.video.R;
 import com.yc.video.config.ConstantKeys;
 import com.yc.video.config.VideoPlayerConfig;
 import com.yc.video.controller.BaseVideoController;
-
 import com.yc.kernel.inter.AbstractVideoPlayer;
 import com.yc.kernel.factory.PlayerFactory;
-
 import com.yc.video.surface.InterSurfaceView;
 import com.yc.video.surface.SurfaceFactory;
 import com.yc.video.tool.BaseToast;
 import com.yc.video.tool.PlayerUtils;
 import com.yc.video.tool.VideoException;
-
 import com.yc.kernel.inter.VideoPlayerListener;
 import com.yc.kernel.utils.PlayerConstant;
 import com.yc.kernel.utils.VideoLogUtils;
-
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
@@ -362,13 +357,16 @@ public class VideoPlayer<P extends AbstractVideoPlayer> extends FrameLayout
      */
     protected void addDisplay() {
         if (mRenderView != null) {
+            //从容器中移除渲染view
             mPlayerContainer.removeView(mRenderView.getView());
+            //释放资源
             mRenderView.release();
         }
         //创建TextureView对象
         mRenderView = mRenderViewFactory.createRenderView(mContext);
         //绑定mMediaPlayer对象
         mRenderView.attachToPlayer(mMediaPlayer);
+        //添加渲染view到Container布局中
         LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                 ViewGroup.LayoutParams.MATCH_PARENT, Gravity.CENTER);
         mPlayerContainer.addView(mRenderView.getView(), 0, params);
@@ -383,9 +381,13 @@ public class VideoPlayer<P extends AbstractVideoPlayer> extends FrameLayout
             //重新设置option,media player reset之后,option会失效
             setOptions();
         }
+        //播放数据是否设置成功
         if (prepareDataSource()) {
+            //准备开始播放
             mMediaPlayer.prepareAsync();
+            //更改播放器的播放状态
             setPlayState(ConstantKeys.CurrentState.STATE_PREPARING);
+            //更改播放器播放模式状态
             setPlayerState(isFullScreen() ? ConstantKeys.PlayMode.MODE_FULL_SCREEN :
                     isTinyScreen() ? ConstantKeys.PlayMode.MODE_TINY_WINDOW : ConstantKeys.PlayMode.MODE_NORMAL);
         }
@@ -752,10 +754,20 @@ public class VideoPlayer<P extends AbstractVideoPlayer> extends FrameLayout
     /**
      * 设置视频地址
      */
+    @Override
     public void setUrl(String url) {
         setUrl(url, null);
     }
 
+    /**
+     * 获取视频地址
+     * @return
+     */
+    @Override
+    public String getUrl(){
+        return this.mUrl;
+    }
+
     /**
      * 设置包含请求头信息的视频地址
      *

+ 82 - 16
VideoPlayer/src/main/java/com/yc/video/ui/view/BasisVideoController.java

@@ -19,18 +19,21 @@ import android.app.Activity;
 import android.content.Context;
 import android.content.pm.ActivityInfo;
 import android.util.AttributeSet;
+import android.util.Log;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.animation.Animation;
 import android.widget.FrameLayout;
 import android.widget.ImageView;
 import android.widget.ProgressBar;
+import android.widget.TextView;
 
 import androidx.annotation.AttrRes;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import com.yc.video.config.ConstantKeys;
+import com.yc.video.config.VideoPlayerConfig;
 import com.yc.video.controller.GestureVideoController;
 import com.yc.video.tool.BaseToast;
 import com.yc.video.tool.PlayerUtils;
@@ -54,8 +57,15 @@ public class BasisVideoController extends GestureVideoController implements View
     private ImageView mLockButton;
     private ProgressBar mLoadingProgress;
     private ImageView thumb;
-    private CustomBottomView bottomView;
     private CustomTitleView titleView;
+    private CustomBottomView vodControlView;
+    private CustomLiveControlView liveControlView;
+    private CustomOncePlayView customOncePlayView;
+    private TextView tvLiveWaitMessage;
+    /**
+     * 是否是直播,默认不是
+     */
+    public static boolean IS_LIVE = false;
 
     public BasisVideoController(@NonNull Context context) {
         this(context, null);
@@ -101,8 +111,10 @@ public class BasisVideoController extends GestureVideoController implements View
         setEnableInNormal(true);
         //滑动调节亮度,音量,进度,默认开启
         setGestureEnabled(true);
-
-        addDefaultControlComponent("",false);
+        //先移除多有的视图view
+        removeAllControlComponent();
+        //添加视图到界面
+        addDefaultControlComponent("");
     }
 
 
@@ -111,9 +123,8 @@ public class BasisVideoController extends GestureVideoController implements View
      * 快速添加各个组件
      * 需要注意各个层级
      * @param title                             标题
-     * @param isLive                            是否为直播
      */
-    public void addDefaultControlComponent(String title, boolean isLive) {
+    public void addDefaultControlComponent(String title) {
         //添加自动完成播放界面view
         CustomCompleteView completeView = new CustomCompleteView(mContext);
         completeView.setVisibility(GONE);
@@ -136,23 +147,61 @@ public class BasisVideoController extends GestureVideoController implements View
         titleView.setVisibility(VISIBLE);
         this.addControlComponent(titleView);
 
-        if (isLive) {
+        //添加直播/回放视频底部控制视图
+        changePlayType();
+
+        //添加滑动控制视图
+        CustomGestureView gestureControlView = new CustomGestureView(mContext);
+        this.addControlComponent(gestureControlView);
+    }
+
+
+    /**
+     * 切换直播/回放类型
+     */
+    public void changePlayType(){
+        if (IS_LIVE) {
             //添加底部播放控制条
-            CustomLiveControlView liveControlView = new CustomLiveControlView(mContext);
+            if (liveControlView==null){
+                liveControlView = new CustomLiveControlView(mContext);
+            }
+            this.removeControlComponent(liveControlView);
             this.addControlComponent(liveControlView);
+
+            //添加直播还未开始视图
+            if (customOncePlayView==null){
+                customOncePlayView = new CustomOncePlayView(mContext);
+                tvLiveWaitMessage = customOncePlayView.getTvMessage();
+            }
+            this.removeControlComponent(customOncePlayView);
+            this.addControlComponent(customOncePlayView);
+
+            //直播视频,移除回放视图
+            if (vodControlView!=null){
+                this.removeControlComponent(vodControlView);
+            }
         } else {
             //添加底部播放控制条
-            bottomView = new CustomBottomView(mContext);
-            //是否显示底部进度条。默认显示
-            bottomView.showBottomProgress(true);
-            this.addControlComponent(bottomView);
+            if (vodControlView==null){
+                vodControlView = new CustomBottomView(mContext);
+                //是否显示底部进度条。默认显示
+                vodControlView.showBottomProgress(true);
+            }
+            this.removeControlComponent(vodControlView);
+            this.addControlComponent(vodControlView);
+
+            //正常视频,移除直播视图
+            if (liveControlView!=null){
+                this.removeControlComponent(liveControlView);
+            }
+            if (customOncePlayView!=null){
+                this.removeControlComponent(customOncePlayView);
+            }
         }
-        //添加滑动控制视图
-        CustomGestureView gestureControlView = new CustomGestureView(mContext);
-        this.addControlComponent(gestureControlView);
-        setCanChangePosition(!isLive);
+        setCanChangePosition(!IS_LIVE);
     }
 
+
     @Override
     public void onClick(View v) {
         int i = v.getId();
@@ -297,6 +346,17 @@ public class BasisVideoController extends GestureVideoController implements View
         return super.onBackPressed();
     }
 
+    /**
+     * 刷新进度回调,子类可在此方法监听进度刷新,然后更新ui
+     *
+     * @param duration 视频总时长
+     * @param position 视频当前时长
+     */
+    @Override
+    protected void setProgress(int duration, int position) {
+        super.setProgress(duration, position);
+    }
+
     @Override
     public void destroy() {
 
@@ -313,6 +373,12 @@ public class BasisVideoController extends GestureVideoController implements View
     }
 
     public CustomBottomView getBottomView() {
-        return bottomView;
+        return vodControlView;
     }
+
+
+    public TextView getTvLiveWaitMessage() {
+        return tvLiveWaitMessage;
+    }
+
 }

+ 39 - 0
VideoPlayer/src/main/java/com/yc/video/ui/view/CustomBottomView.java

@@ -20,6 +20,7 @@ import android.content.Context;
 import android.content.pm.ActivityInfo;
 import android.os.Build;
 import android.util.AttributeSet;
+import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
@@ -38,6 +39,7 @@ import androidx.annotation.Nullable;
 
 import com.yc.video.config.ConstantKeys;
 import com.yc.video.bridge.ControlWrapper;
+import com.yc.video.config.VideoPlayerConfig;
 import com.yc.video.tool.PlayerUtils;
 
 import com.yc.video.R;
@@ -181,6 +183,7 @@ public class CustomBottomView extends FrameLayout implements InterControlView,
             case ConstantKeys.CurrentState.STATE_PREPARING:
             case ConstantKeys.CurrentState.STATE_PREPARED:
             case ConstantKeys.CurrentState.STATE_ERROR:
+            case ConstantKeys.CurrentState.STATE_ONCE_LIVE:
                 setVisibility(GONE);
                 break;
             case ConstantKeys.CurrentState.STATE_PLAYING:
@@ -238,6 +241,12 @@ public class CustomBottomView extends FrameLayout implements InterControlView,
         }
     }
 
+    /**
+     * 刷新进度回调,子类可在此方法监听进度刷新,然后更新ui
+     *
+     * @param duration 视频总时长
+     * @param position 视频当前时长
+     */
     @Override
     public void setProgress(int duration, int position) {
         if (mIsDragging) {
@@ -270,6 +279,25 @@ public class CustomBottomView extends FrameLayout implements InterControlView,
         if (mTvCurrTime != null){
             mTvCurrTime.setText(PlayerUtils.formatTime(position));
         }
+
+
+        if (VideoPlayerConfig.newBuilder().build().mIsShowToast){
+            long time = VideoPlayerConfig.newBuilder().build().mShowToastTime;
+            if (time<=0){
+                time = 5;
+            }
+            long currentPosition = mControlWrapper.getCurrentPosition();
+            Log.d("progress---","duration---"+duration+"--currentPosition--"+currentPosition);
+            if (duration - currentPosition <  2 * time * 1000){
+                //当前视频播放到最后3s时,弹出toast提示:即将自动为您播放下一个视频。
+                if ((duration-currentPosition) / 1000 % 60 == time){
+                    Log.d("progress---","即将自动为您播放下一个视频");
+                    if (listener!= null){
+                        listener.showToastOrDialog();
+                    }
+                }
+            }
+        }
     }
 
     @Override
@@ -314,4 +342,15 @@ public class CustomBottomView extends FrameLayout implements InterControlView,
             mTvCurrTime.setText(PlayerUtils.formatTime(newPosition));
         }
     }
+
+    private OnToastListener listener;
+
+    public void setListener(OnToastListener listener) {
+        this.listener = listener;
+    }
+
+    public interface OnToastListener{
+        void showToastOrDialog();
+    }
+
 }

+ 12 - 0
VideoPlayer/src/main/java/com/yc/video/ui/view/CustomErrorView.java

@@ -113,8 +113,20 @@ public class CustomErrorView extends LinearLayout implements InterControlView {
         if (playState == ConstantKeys.CurrentState.STATE_ERROR) {
             bringToFront();
             setVisibility(VISIBLE);
+            mTvMessage.setText("视频播放异常");
+        } if (playState == ConstantKeys.CurrentState.STATE_NETWORK_ERROR) {
+            bringToFront();
+            setVisibility(VISIBLE);
+            mTvMessage.setText("无网络,请检查网络设置");
+        } if (playState == ConstantKeys.CurrentState.STATE_PARSE_ERROR) {
+            bringToFront();
+            setVisibility(VISIBLE);
+            //mTvMessage.setText("视频解析异常");
+            mTvMessage.setText("视频加载错误");
         } else if (playState == ConstantKeys.CurrentState.STATE_IDLE) {
             setVisibility(GONE);
+        } else if (playState == ConstantKeys.CurrentState.STATE_ONCE_LIVE) {
+            setVisibility(GONE);
         }
     }
 

+ 2 - 1
VideoPlayer/src/main/java/com/yc/video/ui/view/CustomGestureView.java

@@ -176,7 +176,8 @@ public class CustomGestureView extends FrameLayout implements IGestureComponent
                 || playState == ConstantKeys.CurrentState.STATE_PREPARING
                 || playState == ConstantKeys.CurrentState.STATE_PREPARED
                 || playState == ConstantKeys.CurrentState.STATE_ERROR
-                || playState == ConstantKeys.CurrentState.STATE_BUFFERING_PLAYING) {
+                || playState == ConstantKeys.CurrentState.STATE_BUFFERING_PLAYING
+                || playState == ConstantKeys.CurrentState.STATE_ONCE_LIVE) {
             setVisibility(GONE);
         } else {
             setVisibility(VISIBLE);

+ 1 - 0
VideoPlayer/src/main/java/com/yc/video/ui/view/CustomLiveControlView.java

@@ -141,6 +141,7 @@ public class CustomLiveControlView extends FrameLayout implements InterControlVi
             case ConstantKeys.CurrentState.STATE_PREPARED:
             case ConstantKeys.CurrentState.STATE_ERROR:
             case ConstantKeys.CurrentState.STATE_BUFFERING_PLAYING:
+            case ConstantKeys.CurrentState.STATE_ONCE_LIVE:
                 setVisibility(GONE);
                 break;
             case ConstantKeys.CurrentState.STATE_PLAYING:

+ 173 - 0
VideoPlayer/src/main/java/com/yc/video/ui/view/CustomOncePlayView.java

@@ -0,0 +1,173 @@
+/*
+Copyright 2017 yangchong211(github.com/yangchong211)
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+package com.yc.video.ui.view;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.animation.Animation;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.yc.video.R;
+import com.yc.video.bridge.ControlWrapper;
+import com.yc.video.config.ConstantKeys;
+import com.yc.video.tool.BaseToast;
+import com.yc.video.tool.PlayerUtils;
+
+
+/**
+ * <pre>
+ *     @author yangchong
+ *     blog  : https://github.com/yangchong211
+ *     time  : 2017/11/9
+ *     desc  : 即将开播视图
+ *     revise:
+ * </pre>
+ */
+public class CustomOncePlayView extends LinearLayout implements InterControlView {
+
+    private Context mContext;
+    private float mDownX;
+    private float mDownY;
+    private TextView mTvMessage;
+    private TextView mTvRetry;
+    private int playState;
+    private ControlWrapper mControlWrapper;
+
+    public CustomOncePlayView(Context context) {
+        super(context);
+        init(context);
+    }
+
+    public CustomOncePlayView(Context context, @Nullable AttributeSet attrs) {
+        super(context, attrs);
+        init(context);
+    }
+
+    public CustomOncePlayView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        init(context);
+    }
+
+
+    private void init(Context context){
+        this.mContext = context;
+        setVisibility(GONE);
+        View view = LayoutInflater.from(getContext()).inflate(
+                R.layout.custom_video_player_once_live, this, true);
+        initFindViewById(view);
+        initListener();
+        setClickable(true);
+    }
+
+    private void initFindViewById(View view) {
+        mTvMessage = view.findViewById(R.id.tv_message);
+        mTvRetry = view.findViewById(R.id.tv_retry);
+    }
+
+    private void initListener() {
+        mTvRetry.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                if (playState == ConstantKeys.CurrentState.STATE_ONCE_LIVE){
+                    //即将开播
+                    if (PlayerUtils.isConnected(mContext)){
+                        mControlWrapper.start();
+                    } else {
+                        BaseToast.showRoundRectToast("请查看网络是否连接");
+                    }
+                } else {
+                    BaseToast.showRoundRectToast("时间还未到,请稍后再试");
+                }
+            }
+        });
+    }
+
+    @Override
+    public void attach(@NonNull ControlWrapper controlWrapper) {
+        mControlWrapper = controlWrapper;
+    }
+
+    @Override
+    public View getView() {
+        return this;
+    }
+
+    @Override
+    public void onVisibilityChanged(boolean isVisible, Animation anim) {
+
+    }
+
+    @Override
+    public void onPlayStateChanged(int playState) {
+        this.playState = playState;
+        if (playState == ConstantKeys.CurrentState.STATE_ONCE_LIVE) {
+            //即将开播
+            setVisibility(VISIBLE);
+        } else {
+            setVisibility(GONE);
+        }
+    }
+
+    @Override
+    public void onPlayerStateChanged(int playerState) {
+
+    }
+
+    @Override
+    public void setProgress(int duration, int position) {
+
+    }
+
+    @Override
+    public void onLockStateChanged(boolean isLock) {
+
+    }
+
+    @Override
+    public boolean dispatchTouchEvent(MotionEvent ev) {
+        switch (ev.getAction()) {
+            case MotionEvent.ACTION_DOWN:
+                mDownX = ev.getX();
+                mDownY = ev.getY();
+                getParent().requestDisallowInterceptTouchEvent(true);
+                break;
+            case MotionEvent.ACTION_MOVE:
+                float absDeltaX = Math.abs(ev.getX() - mDownX);
+                float absDeltaY = Math.abs(ev.getY() - mDownY);
+                if (absDeltaX > ViewConfiguration.get(getContext()).getScaledTouchSlop() ||
+                        absDeltaY > ViewConfiguration.get(getContext()).getScaledTouchSlop()) {
+                    getParent().requestDisallowInterceptTouchEvent(false);
+                }
+            case MotionEvent.ACTION_UP:
+                break;
+        }
+        return super.dispatchTouchEvent(ev);
+    }
+
+
+    public TextView getTvMessage() {
+        return mTvMessage;
+    }
+
+}

+ 1 - 0
VideoPlayer/src/main/java/com/yc/video/ui/view/CustomPrepareView.java

@@ -142,6 +142,7 @@ public class CustomPrepareView extends FrameLayout implements InterControlView {
             case ConstantKeys.CurrentState.STATE_BUFFERING_PAUSED:
             case ConstantKeys.CurrentState.STATE_COMPLETED:
             case ConstantKeys.CurrentState.STATE_BUFFERING_PLAYING:
+            case ConstantKeys.CurrentState.STATE_ONCE_LIVE:
                 setVisibility(GONE);
                 break;
             case ConstantKeys.CurrentState.STATE_IDLE:

+ 1 - 1
VideoPlayer/src/main/java/com/yc/video/ui/view/CustomTitleView.java

@@ -123,7 +123,7 @@ public class CustomTitleView extends FrameLayout implements InterControlView, Vi
         if (title!=null && title.length()>0){
             mTvTitle.setText(title);
         } else {
-            mTvTitle.setText("视频");
+            mTvTitle.setText("");
         }
     }
 

+ 1 - 1
VideoPlayer/src/main/res/layout/custom_video_player_completed.xml

@@ -5,7 +5,7 @@
     android:id="@+id/complete_container"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:background="#33000000">
+    android:background="@android:color/black">
 
     <ImageView
         android:id="@+id/iv_stop_fullscreen"

+ 3 - 3
VideoPlayer/src/main/res/layout/custom_video_player_error.xml

@@ -16,14 +16,14 @@
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:gravity="center"
-            android:textSize="14sp"
+            android:textSize="16sp"
             android:text="@string/error_message"
             android:textColor="@android:color/white" />
 
         <TextView
             android:id="@+id/tv_retry"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
+            android:layout_width="110dp"
+            android:layout_height="40dp"
             android:layout_marginTop="16dp"
             android:background="@drawable/bg_retry"
             android:gravity="center"

+ 50 - 0
VideoPlayer/src/main/res/layout/custom_video_player_once_live.xml

@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="@android:color/black">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:gravity="center"
+        android:orientation="vertical">
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:gravity="center"
+            android:textSize="16sp"
+            android:text="开播倒计时"
+            android:textColor="@android:color/white" />
+
+        <TextView
+            android:id="@+id/tv_message"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="10dp"
+            android:gravity="center"
+            android:textSize="16sp"
+            android:text="10:20秒"
+            android:textColor="@android:color/white" />
+
+        <TextView
+            android:id="@+id/tv_retry"
+            android:layout_width="110dp"
+            android:layout_height="40dp"
+            android:layout_marginTop="16dp"
+            android:layout_gravity="center"
+            android:drawablePadding="2dp"
+            android:background="@drawable/bg_retry"
+            android:gravity="center"
+            android:paddingLeft="12dp"
+            android:paddingTop="4dp"
+            android:paddingRight="12dp"
+            android:paddingBottom="4dp"
+            android:textSize="14sp"
+            android:text="即将开播"
+            android:textColor="@android:color/white" />
+    </LinearLayout>
+</FrameLayout>
+

+ 1 - 0
VideoSqlHelper/.gitignore

@@ -0,0 +1 @@
+/build

+ 27 - 0
VideoSqlHelper/build.gradle

@@ -0,0 +1,27 @@
+apply plugin: 'com.android.library'
+
+android {
+    compileSdkVersion 29
+    buildToolsVersion "29.0.3"
+
+    defaultConfig {
+        minSdkVersion 17
+        targetSdkVersion 29
+        versionCode 1
+        versionName "1.0"
+
+        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+        consumerProguardFiles "consumer-rules.pro"
+    }
+
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+        }
+    }
+}
+
+dependencies {
+    implementation fileTree(dir: "libs", include: ["*.jar"])
+}

+ 0 - 0
VideoSqlHelper/consumer-rules.pro


+ 21 - 0
VideoSqlHelper/proguard-rules.pro

@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile

+ 7 - 0
VideoSqlHelper/src/main/AndroidManifest.xml

@@ -0,0 +1,7 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.yc.database">
+
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
+
+</manifest>

+ 33 - 0
VideoSqlHelper/src/main/java/com/yc/database/annotation/Column.java

@@ -0,0 +1,33 @@
+package com.yc.database.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+
+/**
+ * <pre>
+ *     @author yangchong
+ *     email  : yangchong211@163.com
+ *     time  : 2017/8/6
+ *     desc  : 实体字段不设置该注解,则默认已字段名作为列名
+ *     revise: 此注解配置数据库指定列名和字段默认值,不配置columnName则默认以字段名作为列名,不配置defaultValue则无列默认值
+ * </pre>
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.FIELD)
+public @interface Column {
+
+	/**
+	 * 配置该字段映射到数据库中的列名,不配置默认为字段名
+	 * @return
+	 */
+	String columnName() default "";
+	/**
+	 * 字段默认值
+	 * @return
+	 */
+	String defaultValue() default "";
+
+}

+ 22 - 0
VideoSqlHelper/src/main/java/com/yc/database/annotation/NotDBColumn.java

@@ -0,0 +1,22 @@
+package com.yc.database.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+
+/**
+ * <pre>
+ *     @author yangchong
+ *     email  : yangchong211@163.com
+ *     time  : 2017/8/6
+ *     desc  : 标识哪些字段不映射为数据库列
+ *     revise:
+ * </pre>
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.FIELD)
+public @interface NotDBColumn {
+
+}

+ 33 - 0
VideoSqlHelper/src/main/java/com/yc/database/annotation/PrimaryKey.java

@@ -0,0 +1,33 @@
+package com.yc.database.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+
+/**
+ * <pre>
+ *     @author yangchong
+ *     email  : yangchong211@163.com
+ *     time  : 2017/8/6
+ *     desc  : 主键配置,必须配置该注解,不配置columnName则默认以字段名作为列名,autoGenerate表示主键是否为自增长,默认为是
+ *     revise:
+ * </pre>
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.FIELD)
+public @interface PrimaryKey {
+
+	/**
+	 * 配置该字段映射到数据库中的列名,不配置默认为字段名
+	 * @return
+	 */
+	String columnName() default "";
+	/**
+	 * 该主键是否设置为自增长,默认为否
+	 * @return
+	 */
+	boolean isAutoGenerate() default false;
+
+}

+ 28 - 0
VideoSqlHelper/src/main/java/com/yc/database/annotation/Table.java

@@ -0,0 +1,28 @@
+package com.yc.database.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+
+/**
+ * <pre>
+ *     @author yangchong
+ *     email  : yangchong211@163.com
+ *     time  : 2017/8/6
+ *     desc  : 表名注解,实体不设置此注解,或者设置了此注解但name不设置则默认以实体类名作为表名
+ *     revise:
+ * </pre>
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface Table {
+
+	/**
+	 * 自定义表名
+	 * @return
+	 */
+	String name() default "";
+
+}

+ 53 - 0
VideoSqlHelper/src/main/java/com/yc/database/bean/BindSQL.java

@@ -0,0 +1,53 @@
+package com.yc.database.bean;
+
+
+/**
+ * <pre>
+ *     @author yangchong
+ *     email  : yangchong211@163.com
+ *     time  : 2017/8/6
+ *     desc  : Bind SQL信息,包括执行SQL语句以及执行参数
+ *     revise:
+ * </pre>
+ */
+public class BindSQL {
+	/**
+	 * 执行语句(参数使用占位符),表名不能使用占位符
+	 */
+	private String sql;
+	/**
+	 * 占位符绑定参数
+	 */
+	private Object[] bindArgs;
+	
+	public BindSQL() {
+	}
+	
+	public BindSQL(String sql) {
+		super();
+		this.setSql(sql);
+	}
+
+	public BindSQL(String sql, Object[] bindArgs) {
+		super();
+		this.setSql(sql);
+		this.setBindArgs(bindArgs);
+	}
+
+	public Object[] getBindArgs() {
+		return bindArgs;
+	}
+
+	public void setBindArgs(Object[] bindArgs) {
+		this.bindArgs = bindArgs;
+	}
+
+	public String getSql() {
+		return sql;
+	}
+
+	public void setSql(String sql) {
+		this.sql = sql;
+	}
+
+}

+ 62 - 0
VideoSqlHelper/src/main/java/com/yc/database/bean/EntityTable.java

@@ -0,0 +1,62 @@
+package com.yc.database.bean;
+
+import java.util.LinkedHashMap;
+
+
+/**
+ * <pre>
+ *     @author yangchong
+ *     email  : yangchong211@163.com
+ *     time  : 2017/8/6
+ *     desc  : 实体类信息
+ *     revise:
+ * </pre>
+ */
+public final class EntityTable {
+	/**
+	 * 实体类Class对象
+	 */
+	private Class<?> mClass;
+	/**
+	 * 实体类对应的表名
+	 */
+	private String mTableName;
+	/**
+	 * 实体类对应的表主键
+	 */
+	private PrimaryKey mPrimaryKey;
+	/**
+	 * 实体类对应的表字段集合<column(字段名), {@link Property}>,不包括主键
+	 */
+	private LinkedHashMap<String, Property> mColumnMap;
+	
+	public EntityTable(Class<?> mClass) {
+		super();
+		this.mClass = mClass;
+		mColumnMap = new LinkedHashMap<String, Property>();
+	}
+
+	public String getTableName() {
+		return mTableName;
+	}
+
+	public void setTableName(String mTableName) {
+		this.mTableName = mTableName;
+	}
+
+	public LinkedHashMap<String, Property> getColumnMap() {
+		return mColumnMap;
+	}
+
+	public PrimaryKey getPrimaryKey() {
+		return mPrimaryKey;
+	}
+
+	public void setPrimaryKey(PrimaryKey mPrimaryKey) {
+		this.mPrimaryKey = mPrimaryKey;
+	}
+
+	public Class<?> getEntityClass() {
+		return mClass;
+	}
+}

+ 26 - 0
VideoSqlHelper/src/main/java/com/yc/database/bean/PrimaryKey.java

@@ -0,0 +1,26 @@
+package com.yc.database.bean;
+
+
+/**
+ * <pre>
+ *     @author yangchong
+ *     email  : yangchong211@163.com
+ *     time  : 2017/8/6
+ *     desc  : 主键字段
+ *     revise:
+ * </pre>
+ */
+public class PrimaryKey extends Property {
+	/**
+	 * 当前主键是否为自增长
+	 */
+	private boolean isAutoGenerate;
+
+	public boolean isAutoGenerate() {
+		return isAutoGenerate;
+	}
+
+	public void setAutoGenerate(boolean isAutoGenerate) {
+		this.isAutoGenerate = isAutoGenerate;
+	}
+}

+ 185 - 0
VideoSqlHelper/src/main/java/com/yc/database/bean/Property.java

@@ -0,0 +1,185 @@
+package com.yc.database.bean;
+
+import android.database.Cursor;
+
+import java.lang.reflect.Field;
+import java.util.Date;
+
+import com.yc.database.manager.FieldTypeManager;
+import com.yc.database.utils.DateUtil;
+import com.yc.database.utils.ValueUtil;
+
+
+/**
+ * <pre>
+ *     @author yangchong
+ *     email  : yangchong211@163.com
+ *     time  : 2017/8/6
+ *     desc  : 实体属性字段
+ *     revise:
+ * </pre>
+ */
+public class Property {
+	/**
+	 * 字段名,建表时用
+	 */
+	private String column;
+	/**
+	 * 默认值,建表时要设置的字段默认值
+	 */
+	private String defaultValue;
+	/**
+	 * 该字段对应实体信息中的属性字段信息
+	 */
+	private Field field;
+	
+	/**
+	 * 获取指定对象的当前字段的值
+	 * Author: hyl
+	 * Time: 2015-8-16下午10:23:35
+	 * @param entity	获取字段值的对象
+	 * @return
+	 */
+	@SuppressWarnings("unchecked")
+	public <T> T getValue(Object entity) {
+		if(entity != null) {
+			try {
+				field.setAccessible(true);
+				return (T) field.get(entity);
+			} catch (IllegalArgumentException e) {
+				e.printStackTrace();
+			} catch (IllegalAccessException e) {
+				e.printStackTrace();
+			}
+		}
+		return null;
+	}
+	
+	/**
+	 * 设置指定对象的当前字段的值
+	 * Author: hyl
+	 * Time: 2015-8-16下午10:24:08
+	 * @param entity	要设置字段值的对象
+	 * @param value		要设置的值
+	 * @throws Exception
+	 */
+	public void setValue(Object entity, Object value) throws Exception {
+		int fieldType = FieldTypeManager.getFieldType(field);
+		try {
+			field.setAccessible(true);
+			switch(fieldType) {
+				case FieldTypeManager.BASE_TYPE_BOOLEAN:
+					field.set(entity, Boolean.parseBoolean(value.toString()));
+					break;
+				case FieldTypeManager.BASE_TYPE_BYTE_ARRAY:
+					field.set(entity, (byte[])value);
+					break;
+				case FieldTypeManager.BASE_TYPE_CHAR:
+					field.set(entity, value.toString().charAt(0));
+					break;
+				case FieldTypeManager.BASE_TYPE_STRING:
+					field.set(entity, value.toString());
+					break;
+				case FieldTypeManager.BASE_TYPE_DATE:
+					field.set(entity, DateUtil.formatDatetime((Date) value));
+					break;
+				case FieldTypeManager.BASE_TYPE_DOUBLE:
+					field.set(entity, Double.parseDouble(value.toString()));
+					break;
+				case FieldTypeManager.BASE_TYPE_FLOAT:
+					field.set(entity, Float.parseFloat(value.toString()));
+					break;
+				case FieldTypeManager.BASE_TYPE_INT:
+					field.set(entity, Integer.parseInt(value.toString()));
+					break;
+				case FieldTypeManager.BASE_TYPE_LONG:
+					field.set(entity, Long.parseLong(value.toString()));
+					break;
+				case FieldTypeManager.BASE_TYPE_SHORT:
+					field.set(entity, Short.parseShort(value.toString()));
+					break;
+			}
+		} catch (Exception e) {
+			throw e;
+		}
+	}
+	
+	/**
+	 * 设置指定实体对象当前属性字段的值
+	 * Author: hyl
+	 * Time: 2015-8-21上午10:20:14
+	 * @param entity		要设置值的实体对象
+	 * @param cursor		数据来源
+	 * @throws Exception
+	 */
+	public void setValue(Object entity, Cursor cursor) throws Exception {
+		int fieldType = FieldTypeManager.getFieldType(field);
+		try {
+			int columnIdx = cursor.getColumnIndex(column);
+			if(columnIdx == -1) {//当前游标中没有该字段的值
+				return;
+			}
+			String columnValue = cursor.getString(columnIdx);
+			boolean isEmpty = ValueUtil.isEmpty(columnValue);
+			field.setAccessible(true);
+			switch(fieldType) {
+				case FieldTypeManager.BASE_TYPE_BOOLEAN:
+					field.set(entity, isEmpty ? false : Boolean.parseBoolean(columnValue));
+					break;
+				case FieldTypeManager.BASE_TYPE_BYTE_ARRAY:
+					field.set(entity, cursor.getBlob(columnIdx));
+					break;
+				case FieldTypeManager.BASE_TYPE_CHAR:
+					field.set(entity, isEmpty ? Character.valueOf(' ') : columnValue.charAt(0));
+					break;
+				case FieldTypeManager.BASE_TYPE_STRING:
+					field.set(entity, isEmpty ? "" : columnValue);
+					break;
+				case FieldTypeManager.BASE_TYPE_DATE:
+					field.set(entity, isEmpty ? "" : DateUtil.parseDatetime(columnValue));
+					break;
+				case FieldTypeManager.BASE_TYPE_DOUBLE:
+					field.set(entity, cursor.getDouble(columnIdx));
+					break;
+				case FieldTypeManager.BASE_TYPE_FLOAT:
+					field.set(entity, cursor.getFloat(columnIdx));
+					break;
+				case FieldTypeManager.BASE_TYPE_INT:
+					field.set(entity, cursor.getInt(columnIdx));
+					break;
+				case FieldTypeManager.BASE_TYPE_LONG:
+					field.set(entity, cursor.getLong(columnIdx));
+					break;
+				case FieldTypeManager.BASE_TYPE_SHORT:
+					field.set(entity, cursor.getShort(columnIdx));
+					break;
+			}
+		} catch (Exception e) {
+			throw e;
+		}
+	}
+	
+	public String getColumn() {
+		return column;
+	}
+	
+	public void setColumn(String column) {
+		this.column = column;
+	}
+	
+	public String getDefaultValue() {
+		return defaultValue;
+	}
+	
+	public void setDefaultValue(String defaultValue) {
+		this.defaultValue = defaultValue;
+	}
+	
+	public Field getField() {
+		return field;
+	}
+	
+	public void setField(Field field) {
+		this.field = field;
+	}
+}

+ 31 - 0
VideoSqlHelper/src/main/java/com/yc/database/listener/InterDBListener.java

@@ -0,0 +1,31 @@
+package com.yc.database.listener;
+
+import android.database.sqlite.SQLiteDatabase;
+
+
+/**
+ * <pre>
+ *     @author yangchong
+ *     email  : yangchong211@163.com
+ *     time  : 2017/8/6
+ *     desc  : 数据监听(数据库第一次创建,版本变更时的监听)
+ *     revise:
+ * </pre>
+ */
+public interface InterDBListener {
+	/**
+	 * 数据库版本变更的时候执行,在数据库DB打开时会进行版本号判断,当版本号不同的时候会执行该监听处理函数(只会执行一次)
+	 * 在此方法中执行数据库操作不会出现(java.lang.IllegalStateException: getDatabase called recursively)异常,已解决
+	 * @param db			数据库
+	 * @param oldVersion	旧版本号
+	 * @param newVersion	新版本号
+	 */
+	void onUpgradeHandler(SQLiteDatabase db, int oldVersion, int newVersion);
+
+	/**
+	 * 数据库文件第一次创建时的监听响应函数,已经存在的数据库不会执行此方法
+	 * 在此方法中执行数据库操作不会出现(java.lang.IllegalStateException: getDatabase called recursively)异常,已解决
+	 * @param db	数据库
+	 */
+	void onDbCreateHandler(SQLiteDatabase db);
+}

+ 27 - 0
VideoSqlHelper/src/main/java/com/yc/database/listener/SimpleDBListener.java

@@ -0,0 +1,27 @@
+package com.yc.database.listener;
+
+import android.database.sqlite.SQLiteDatabase;
+
+
+/**
+ * <pre>
+ *     @author yangchong
+ *     email  : yangchong211@163.com
+ *     time  : 2017/8/6
+ *     desc  : 数据库监听空实现
+ *     revise:
+ * </pre>
+ */
+public class SimpleDBListener implements InterDBListener {
+
+	@Override
+	public void onUpgradeHandler(SQLiteDatabase db, int oldVersion, int newVersion) {
+
+	}
+
+	@Override
+	public void onDbCreateHandler(SQLiteDatabase db) {
+
+	}
+
+}

+ 270 - 0
VideoSqlHelper/src/main/java/com/yc/database/manager/EntityTableManager.java

@@ -0,0 +1,270 @@
+package com.yc.database.manager;
+
+import android.database.Cursor;
+
+import java.io.Serializable;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+
+import com.yc.database.sql.SQLBuilder;
+import com.yc.database.annotation.Column;
+import com.yc.database.annotation.NotDBColumn;
+import com.yc.database.annotation.PrimaryKey;
+import com.yc.database.bean.EntityTable;
+import com.yc.database.bean.Property;
+import com.yc.database.utils.CursorUtil;
+import com.yc.database.utils.ValueUtil;
+
+
+/**
+ * <pre>
+ *     @author yangchong
+ *     email  : yangchong211@163.com
+ *     time  : 2017/8/6
+ *     desc  : 实体类管理对象
+ *     revise:
+ * </pre>
+ */
+public final class EntityTableManager implements Serializable {
+	/**
+	 * uid
+	 */
+	private static final long serialVersionUID = 1L;
+	/**
+	 * 每张表的相关信息缓存集合<实体类{@link}Class, {@link}Entity>
+	 */
+	private static HashMap<Class<?>, EntityTable> mTableMap = new HashMap<Class<?>, EntityTable>();
+	
+	/**
+	 * 获取EntityTable对象
+	 * @param entity
+	 * @return
+	 */
+	public static <T> EntityTable getEntityTable(T entity) {
+		return getEntityTable(entity.getClass());
+	}
+	
+	/**
+	 * 获取EntityTable对象
+	 * @param mClass
+	 * @return
+	 */
+	public static EntityTable getEntityTable(Class<?> mClass) {
+		if(mTableMap.containsKey(mClass)) {
+			return mTableMap.get(mClass);
+		} else {
+			return createEntityTable(mClass);
+		}
+	}
+
+	/**
+	 * 检查实体类表是否存在,不存在则创建该实体表,线程安全
+	 * @param entity
+	 */
+	public synchronized static <T> void checkOrCreateTable(SQLExecuteManager sqlExecuteManager, T entity) {
+		Class<?> aClass = entity.getClass();
+		checkOrCreateTable(sqlExecuteManager, aClass);
+	}
+	
+	/**
+	 * 检查指定类所表示的表是否存在,不存在则创建该实体表,线程安全
+	 * @param sqlExecuteManager
+	 * @param mClass
+	 */
+	public synchronized static void checkOrCreateTable(SQLExecuteManager sqlExecuteManager, Class<?> mClass) {
+		if(!mTableMap.containsKey(mClass)) {//先在缓存中查找,缓存中不存在
+			EntityTable entityTable = createEntityTable(mClass);
+			mTableMap.put(mClass, entityTable);
+			if(!checkTableIsInDB(sqlExecuteManager, entityTable.getTableName())) {//数据库中不存在
+				createTable(sqlExecuteManager, entityTable);
+			} else {//数据库中存在该表
+				//对已存在表的字段与最新的实体对象进行比较,看是否需要更新表的字段信息
+				checkOrAlterTableColumn(sqlExecuteManager, entityTable);
+			}
+		} else {//缓存中存在实体表映射关系,检查当前数据库中是否存在该表,不存在创建该表
+			if(!checkTableIsInDB(sqlExecuteManager, mTableMap.get(mClass).getTableName())) {//数据库中不存在
+				createTable(sqlExecuteManager, mTableMap.get(mClass));
+			}
+		}
+	}
+	
+	/**
+	 * 检查表的字段信息,实体中有新增字段,则给表添加相应的字段
+	 * @param sqlExecuteManager
+	 * @param entityTable
+	 */
+	public static void checkOrAlterTableColumn(SQLExecuteManager sqlExecuteManager, EntityTable entityTable) {
+		Cursor cursor = sqlExecuteManager.query(SQLBuilder.getTableAllColumnSQL(entityTable.getTableName()));
+		LinkedHashMap<String, Property> propertys = entityTable.getColumnMap();
+		//数据表中已有的字段信息
+		List<String> columns = Arrays.asList(cursor.getColumnNames());
+		
+		List<Property> addColumns = new ArrayList<Property>();
+		for(String key : propertys.keySet()) {
+			Property property = propertys.get(key);
+			if (property!=null){
+				if(!columns.contains(property.getColumn())) {
+					//数据表中不包含当前实体属性字段信息,说明当前实体属性字段需要新增到数据表中
+					addColumns.add(propertys.get(key));
+				}
+			}
+		}
+		
+		if(addColumns.size() > 0) {
+			try {
+				sqlExecuteManager.beginTransaction();
+				for(Property column : addColumns) {
+					sqlExecuteManager.execSQL(SQLBuilder.getAlterTableSQL(entityTable.getTableName(), column));
+				}
+				sqlExecuteManager.successTransaction();
+			} finally {
+				sqlExecuteManager.endTransaction();
+			}
+		}
+	}
+	
+	/**
+	 * 创建实体类
+	 * @param mClass
+	 */
+	public static EntityTable createEntityTable(Class<?> mClass) {
+		EntityTable entity = new EntityTable(mClass);
+		entity.setTableName(SQLBuilder.getTableName(mClass));
+
+		Class<?> claxx = mClass;
+		while(claxx != Object.class) {
+			setEntityTableField(entity, claxx);
+			claxx = claxx.getSuperclass();
+		}
+		
+		if(entity.getPrimaryKey() == null) {
+			throw new RuntimeException("必须为实体" + mClass.getName() + "设置主键---[在要设置主键的字段上添加注解PrimaryKey来设置主键]");
+		}
+		return entity;
+	}
+
+	private static void setEntityTableField(EntityTable entity, Class claxx) {
+		/*
+		 * 获取Class下声明的所有字段信息(包括:public,protected,private,默认级别, 四种访问级别的字段信息)
+		 * 如果该Class没有声明任何字段或是Class代表一个基本类型、数组或void,则返回数组长度为0
+		 * */
+		Field[] fields = claxx.getDeclaredFields();
+		String columnName = "";//字段列名
+		for(int i = 0; i < fields.length; i++) {
+			Field field = fields[i];
+
+			if (Modifier.isStatic(field.getModifiers())) {//过滤掉static静态字段
+				continue;
+			}
+
+			if(FieldTypeManager.getFieldType(field) == FieldTypeManager.NOT_BASE_TYPE) {//过滤掉非基本类型字段
+				continue;
+			}
+
+			if(entity.getPrimaryKey() == null) {//主键设置过后即不再遍历主键注解
+				PrimaryKey key = field.getAnnotation(PrimaryKey.class);
+				if(key != null) {//声明了主键字段
+					if(key.isAutoGenerate() && !FieldTypeManager.isAutoIncrementType(field)) {
+						throw new RuntimeException("自增长主键字段类型不正确,请设置自增长字段类型为long");
+					}
+					if(ValueUtil.isEmpty(key.columnName())) {//没有通过注解设置列名,默认取字段名称为列名
+						columnName = field.getName();
+					} else {
+						columnName = key.columnName();
+					}
+					if(Arrays.binarySearch(SQLExecuteManager.SQLITE_KEYWORDS, columnName.toUpperCase()) >= 0) {
+						throw new IllegalArgumentException("字段名或注解columnName属性不能为SQLite关键字:" + columnName);
+					}
+					com.yc.database.bean.PrimaryKey primaryKey = new com.yc.database.bean.PrimaryKey();
+					primaryKey.setField(field);
+					primaryKey.setColumn(columnName);
+					primaryKey.setAutoGenerate(key.isAutoGenerate());//获取是否自动增长
+					entity.setPrimaryKey(primaryKey);
+					continue;
+				}
+			}
+
+			Property property = new Property();
+			property.setField(field);
+
+			Column column = field.getAnnotation(Column.class);
+			if(column != null) {//声明了 字段  注解
+				if(ValueUtil.isEmpty(column.columnName())) {//没有通过注解设置列名,默认取字段名称为列名
+					columnName = field.getName();
+				} else {
+					columnName = column.columnName();
+				}
+				if(Arrays.binarySearch(SQLExecuteManager.SQLITE_KEYWORDS, columnName.toUpperCase()) >= 0) {
+					throw new IllegalArgumentException("字段名或注解columnName属性不能为SQLite关键字:" + columnName);
+				}
+				if(!ValueUtil.isEmpty(column.defaultValue())) {//设置了字段默认值
+					property.setDefaultValue(column.defaultValue());
+				}
+				property.setColumn(columnName);
+				entity.getColumnMap().put(property.getColumn(), property);
+				continue;
+			}
+
+			NotDBColumn notDbColumn = field.getAnnotation(NotDBColumn.class);
+			if(notDbColumn != null) {//非数据库字段不作处理
+				continue;
+			}
+
+			if(Arrays.binarySearch(SQLExecuteManager.SQLITE_KEYWORDS, field.getName().toUpperCase()) >= 0) {
+				throw new IllegalArgumentException("注解字段名不能为SQLite关键字:" + columnName);
+			}
+
+			//没有设置注解的字段,以字段名为数据库列名
+			property.setColumn(field.getName());
+			entity.getColumnMap().put(property.getColumn(), property);
+		}
+	}
+	
+	/**
+	 * 根据实体类创建数据库表
+	 * @param sqlExecuteManager
+	 * @param entity
+	 */
+	public static void createTable(SQLExecuteManager sqlExecuteManager, EntityTable entity) {
+		dropTable(sqlExecuteManager, entity);//先删除表
+		String createTableSQL = SQLBuilder.getCreateTableSQL(entity);//构造建表语句
+		sqlExecuteManager.execSQL(createTableSQL);//建表
+	}
+	
+	/**
+	 * 删除实体类对应的数据库表
+	 * @param sqlExecuteManager
+	 * @param entity
+	 */
+	public static void dropTable(SQLExecuteManager sqlExecuteManager, EntityTable entity) {
+		sqlExecuteManager.dropTable(entity.getTableName());
+		mTableMap.remove(entity.getClass());
+	}
+	
+	/**
+	 * 清除实体表缓存对象
+	 */
+	public static void clear() {
+		if (mTableMap!=null){
+			mTableMap.clear();
+		}
+	}
+	
+	/**
+	 * 检查数据库中是否存在指定表
+	 * @param sqlExecuteManager
+	 * @param tableName
+	 * @return
+	 */
+	public static boolean checkTableIsInDB(SQLExecuteManager sqlExecuteManager, String tableName) {
+		Cursor cursor = null;
+		cursor = sqlExecuteManager.query(SQLBuilder.getCheckTableExistSQL(tableName));
+		long total = CursorUtil.parseCursorTotal(cursor);
+		return total > 0 ? true : false;
+	}
+}

+ 191 - 0
VideoSqlHelper/src/main/java/com/yc/database/manager/FieldTypeManager.java

@@ -0,0 +1,191 @@
+package com.yc.database.manager;
+
+import java.lang.reflect.Field;
+import java.util.Date;
+
+
+/**
+ * <pre>
+ *     @author yangchong
+ *     email  : yangchong211@163.com
+ *     time  : 2017/8/6
+ *     desc  : 字段类型
+ *     revise:
+ * </pre>
+ */
+public final class FieldTypeManager {
+	/**
+	 * 空值
+	 */
+	public static final int VALUE_TYPE_NULL = -1;
+	/**
+	 * 非数据库支持基本类型
+	 */
+	public static final int NOT_BASE_TYPE = 0;
+	/**
+	 * String
+	 */
+	public static final int BASE_TYPE_STRING = 1;
+	/**
+	 * int
+	 */
+	public static final int BASE_TYPE_INT = 2;
+	/**
+	 * short
+	 */
+	public static final int BASE_TYPE_SHORT = 3;
+	/**
+	 * float
+	 */
+	public static final int BASE_TYPE_FLOAT = 4;
+	/**
+	 * double
+	 */
+	public static final int BASE_TYPE_DOUBLE = 5;
+	/**
+	 * long
+	 */
+	public static final int BASE_TYPE_LONG = 6;
+	/**
+	 * char
+	 */
+	public static final int BASE_TYPE_CHAR = 7;
+	/**
+	 * boolean
+	 */
+	public static final int BASE_TYPE_BOOLEAN = 8;
+	/**
+	 * byte[]
+	 */
+	public static final int BASE_TYPE_BYTE_ARRAY = 9;
+	/**
+	 * Date
+	 */
+	public static final int BASE_TYPE_DATE = 10;
+	
+	/**
+	 * 该字段是否为基本字段类型
+	 * @param field
+	 * @return
+	 */
+	public static int getFieldType(Field field) {
+		Class<?> fieldType = field.getType();
+		if (fieldType == String.class) {
+			return FieldTypeManager.BASE_TYPE_STRING;
+		}
+		if(fieldType == Date.class || fieldType == java.sql.Date.class) {
+			return FieldTypeManager.BASE_TYPE_DATE;
+		}
+		if (fieldType == boolean.class || fieldType == Boolean.class) {
+			return FieldTypeManager.BASE_TYPE_BOOLEAN;
+		}
+		if (fieldType == short.class || fieldType == Short.class) {
+			return FieldTypeManager.BASE_TYPE_SHORT;
+		} 
+		if (fieldType == int.class || fieldType == Integer.class) {
+			return FieldTypeManager.BASE_TYPE_INT;
+		}
+		if (fieldType == long.class || fieldType == Long.class) {
+			return FieldTypeManager.BASE_TYPE_LONG;
+		}
+		if (fieldType == float.class || fieldType == Float.class) {
+			return FieldTypeManager.BASE_TYPE_FLOAT;
+		}
+		if (fieldType == double.class || fieldType == Double.class) {
+			return FieldTypeManager.BASE_TYPE_DOUBLE;
+		}
+		if (fieldType == byte[].class || fieldType == Byte[].class) {
+			return FieldTypeManager.BASE_TYPE_BYTE_ARRAY;
+		}
+		if(fieldType == char.class || fieldType == Character.class) {
+			return FieldTypeManager.BASE_TYPE_CHAR;
+		}
+		return FieldTypeManager.NOT_BASE_TYPE;
+	}
+	
+	/**
+	 * 获取某个值的类型
+	 * @param obj
+	 * @return
+	 */
+	public static int getValueType(Object obj) {
+		if(obj == null) {
+			return FieldTypeManager.VALUE_TYPE_NULL;
+		}
+		if (obj instanceof String) {
+			return FieldTypeManager.BASE_TYPE_STRING;
+		}
+		if(obj instanceof Date || obj instanceof java.sql.Date) {
+			return FieldTypeManager.BASE_TYPE_DATE;
+		}
+		if (obj instanceof Boolean) {
+			return FieldTypeManager.BASE_TYPE_BOOLEAN;
+		}
+		if (obj instanceof Short) {
+			return FieldTypeManager.BASE_TYPE_SHORT;
+		} 
+		if (obj instanceof Integer) {
+			return FieldTypeManager.BASE_TYPE_INT;
+		}
+		if (obj instanceof Long) {
+			return FieldTypeManager.BASE_TYPE_LONG;
+		}
+		if (obj instanceof Float) {
+			return FieldTypeManager.BASE_TYPE_FLOAT;
+		}
+		if (obj instanceof Double) {
+			return FieldTypeManager.BASE_TYPE_DOUBLE;
+		}
+		if (obj instanceof byte[] || obj instanceof Byte[]) {
+			return FieldTypeManager.BASE_TYPE_BYTE_ARRAY;
+		}
+		if(obj instanceof Character) {
+			return FieldTypeManager.BASE_TYPE_CHAR;
+		}
+		return FieldTypeManager.NOT_BASE_TYPE;
+	}
+	
+	/**
+	 * 根据字段类型获取数据库支持的字段类型
+	 * @param field
+	 * @return
+	 */
+	public static String getColumnTypeValue(Field field) {
+		String type = "TEXT";
+		int fieldType = FieldTypeManager.getFieldType(field);
+		switch(fieldType) {
+			case FieldTypeManager.BASE_TYPE_CHAR:
+			case FieldTypeManager.BASE_TYPE_STRING:
+			case FieldTypeManager.BASE_TYPE_DATE:
+				type = "TEXT";
+				break;
+			case FieldTypeManager.BASE_TYPE_BOOLEAN:
+			case FieldTypeManager.BASE_TYPE_INT:
+			case FieldTypeManager.BASE_TYPE_SHORT:
+			case FieldTypeManager.BASE_TYPE_LONG:
+				type = "INTEGER";
+				break;
+			case FieldTypeManager.BASE_TYPE_DOUBLE:
+			case FieldTypeManager.BASE_TYPE_FLOAT:
+				type = "REAL";
+				break;
+			case FieldTypeManager.BASE_TYPE_BYTE_ARRAY:
+				type = "BLOB";
+				break;
+		}
+		return type;
+	}
+	
+	/**
+	 * 当前字段是否为自增长字段类型
+	 * @param field
+	 * @return
+	 */
+	public static boolean isAutoIncrementType(Field field) {
+		int type = getFieldType(field);
+		if(type != BASE_TYPE_LONG) {
+			return false;
+		}
+		return true;
+	}
+}

+ 286 - 0
VideoSqlHelper/src/main/java/com/yc/database/manager/SQLExecuteManager.java

@@ -0,0 +1,286 @@
+package com.yc.database.manager;
+
+import android.annotation.SuppressLint;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteStatement;
+
+import java.io.Serializable;
+import java.util.Date;
+
+import com.yc.database.bean.BindSQL;
+import com.yc.database.utils.DBLog;
+import com.yc.database.utils.DateUtil;
+
+
+/**
+ * <pre>
+ *     @author yangchong
+ *     email  : yangchong211@163.com
+ *     time  : 2017/8/6
+ *     desc  : SQL语句执行器
+ *     revise:
+ * </pre>
+ */
+public class SQLExecuteManager implements Serializable {
+
+	/**
+	 * uid
+	 */
+	private static final long serialVersionUID = 1L;
+	/**
+	 * SQLite中的关键字 
+	 */
+	public static final String[] SQLITE_KEYWORDS = { "ABORT", "ACTION",
+		"ADD", "AFTER", "ALL", "ALTER", "ANALYZE", "AND", "AS", "ASC",
+		"ATTACH", "AUTOINCREMENT", "BEFORE", "BEGIN", "BETWEEN", "BY",
+		"CASCADE", "CASE", "CAST", "CHECK", "COLLATE", "COLUMN", "COMMIT",
+		"CONFLICT", "CONSTRAINT", "CREATE", "CROSS", "CURRENT_DATE",
+		"CURRENT_TIME", "CURRENT_TIMESTAMP", "DATABASE", "DEFAULT",
+		"DEFERRABLE", "DEFERRED", "DELETE", "DESC", "DETACH", "DISTINCT",
+		"DROP", "EACH", "ELSE", "END", "ESCAPE", "EXCEPT", "EXCLUSIVE",
+		"EXISTS", "EXPLAIN", "FAIL", "FOR", "FOREIGN", "FROM", "FULL",
+		"GLOB", "GROUP", "HAVING", "IF", "IGNORE", "IMMEDIATE", "IN",
+		"INDEX", "INDEXED", "INITIALLY", "INNER", "INSERT", "INSTEAD",
+		"INTERSECT", "INTO", "IS", "ISNULL", "JOIN", "KEY", "LEFT", "LIKE",
+		"LIMIT", "MATCH", "NATURAL", "NO", "NOT", "NOTNULL", "NULL", "OF",
+		"OFFSET", "ON", "OR", "ORDER", "OUTER", "PLAN", "PRAGMA",
+		"PRIMARY", "QUERY", "RAISE", "REFERENCES", "REGEXP", "REINDEX",
+		"RELEASE", "RENAME", "REPLACE", "RESTRICT", "RIGHT", "ROLLBACK",
+		"ROW", "SAVEPOINT", "SELECT", "SET", "TABLE", "TEMP", "TEMPORARY",
+		"THEN", "TO", "TRANSACTION", "TRIGGER", "UNION", "UNIQUE",
+		"UPDATE", "USING", "VACUUM", "VALUES", "VIEW", "VIRTUAL", "WHEN",
+		"WHERE" };
+	
+	/**
+	 * 数据库操作类
+	 */
+	private SQLiteDatabase mSQLiteDataBase;
+
+	public SQLExecuteManager(SQLiteDatabase mSQLiteDataBase) {
+		super();
+		this.mSQLiteDataBase = mSQLiteDataBase;
+	}
+	
+	/**
+	 * 开启一个事务(事务开始)
+	 * 在事务代码执行完成后,必须要执行successTransaction()将事务标记为成功
+	 * 在代码的最后必须要执行endTransaction()来结束当前事务,如果事务成功则提交事务,否则回滚事务
+	 * <pre>
+	 *   db.beginTransaction();
+	 *   try {
+	 *     ...
+	 *     db.setTransactionSuccessful();
+	 *   } finally {
+	 *     db.endTransaction();
+	 *   }
+	 * </pre>
+	 */
+	public void beginTransaction() {
+		this.mSQLiteDataBase.beginTransaction();
+	}
+	
+	/**
+	 * 标记当前事务成功
+	 */
+	public void successTransaction() {
+		this.mSQLiteDataBase.setTransactionSuccessful();
+	}
+	
+	/**
+	 * 结束当前事务,当事物被标记成功后,此操作会提交事务,否则会回滚事务
+	 */
+	public void endTransaction() {
+		this.mSQLiteDataBase.endTransaction();
+	}
+
+	/**
+	 * 执行指定无返回值的单条SQL语句,如建表、创建数据库等
+	 * @param sql						执行sql
+	 */
+	public void execSQL(String sql) {
+		DBLog.debug(sql);
+		this.mSQLiteDataBase.execSQL(sql);
+	}
+	
+	/**
+	 * 插入一条记录,返回该记录的rowId
+	 * @param sql
+	 * @param args
+	 * @return				插入失败返回-1,成功返回rowId
+	 */
+	public long insert(String sql, Object[] args) {
+		long rowId = -1;
+		SQLiteStatement statement = this.mSQLiteDataBase.compileStatement(sql);
+		try {
+			if(args != null) {
+				for(int i = 0; i < args.length; i++) {
+					bindArgs(statement, i + 1, args[i]);
+				}
+			}
+			rowId = statement.executeInsert();
+			DBLog.debug(sql, args);
+		} finally {
+			statement.close();
+		}
+		return rowId;
+	}
+	
+	/**
+	 * 根据BindSQL进行插入数据
+	 * @param bindSQL
+	 * @return
+	 * @throws Exception
+	 */
+	public long insert(BindSQL bindSQL) {
+		if (bindSQL==null){
+			return -1;
+		}
+		String sql = bindSQL.getSql();
+		Object[] bindArgs = bindSQL.getBindArgs();
+		return insert(sql, bindArgs);
+	}
+	
+	/**
+	 * 绑定参数
+	 * @param statement
+	 * @param position
+	 * @param args
+	 */
+	private void bindArgs(SQLiteStatement statement, int position, Object args) {
+		int type = FieldTypeManager.getValueType(args);
+		switch(type) {
+			case FieldTypeManager.VALUE_TYPE_NULL:
+				statement.bindNull(position);
+				break;
+			case FieldTypeManager.BASE_TYPE_BYTE_ARRAY:
+				statement.bindBlob(position, (byte[])args);
+				break;
+			case FieldTypeManager.BASE_TYPE_CHAR:
+			case FieldTypeManager.BASE_TYPE_STRING:
+				statement.bindString(position, args.toString());
+				break;
+			case FieldTypeManager.BASE_TYPE_DATE:
+				statement.bindString(position, DateUtil.formatDatetime((Date) args));
+				break;
+			case FieldTypeManager.BASE_TYPE_DOUBLE:
+			case FieldTypeManager.BASE_TYPE_FLOAT:
+				statement.bindDouble(position, Double.parseDouble(args.toString()));
+				break;
+			case FieldTypeManager.BASE_TYPE_INT:
+			case FieldTypeManager.BASE_TYPE_LONG:
+			case FieldTypeManager.BASE_TYPE_SHORT:
+				statement.bindLong(position, Long.parseLong(args.toString()));
+				break;
+			case FieldTypeManager.NOT_BASE_TYPE:
+				throw new IllegalArgumentException("未知参数类型,请检查绑定参数");
+		}
+	}
+	
+	/**
+	 * 删除指定表
+	 * @param tableName
+	 * @throws Exception
+	 */
+	public void dropTable(String tableName) {
+		String sql = "DROP TABLE IF EXISTS " + tableName;
+		execSQL(sql);
+	}
+	
+	/**
+	 * 删除,表名不能使用占位符
+	 * @param bindSQL	
+	 */
+	public void delete(BindSQL bindSQL) {
+		updateOrDelete(bindSQL.getSql(), bindSQL.getBindArgs());
+	}
+	
+	/**
+	 * 删除,表名不能使用占位符
+	 * @param sql		删除语句(参数使用占位符)
+	 * @param args		占位符参数
+	 */
+	@SuppressLint("NewApi")
+	public void updateOrDelete(String sql, Object[] args) {
+		SQLiteStatement statement = mSQLiteDataBase.compileStatement(sql);
+		try {
+			if(args != null) {
+				for(int i = 0; i < args.length; i++) {
+					bindArgs(statement, i + 1, args[i]);
+				}
+			}
+			DBLog.debug(sql, args);
+			statement.executeUpdateDelete();
+		} finally {
+			statement.close();
+		}
+	}
+	
+	/**
+	 * 删除(对于表名需要动态获取的,此方法非常适合)
+	 * @param tableName			要删除的数据表
+	 * @param whereClause		where后面的条件句(delete from XXX where XXX),参数使用占位符
+	 * @param whereArgs			where子句后面的占位符参数
+	 */
+	public void delete(String tableName, String whereClause, String[] whereArgs) {
+		DBLog.debug("{SQL:DELETE FROM " + tableName + " WHERE " + whereClause + ",PARAMS:" + whereArgs + "}");
+		mSQLiteDataBase.delete(tableName, whereClause, whereArgs);
+	}
+	
+	/**
+	 * 更新
+	 * @param bindSQL
+	 */
+	public void update(BindSQL bindSQL) {
+		updateOrDelete(bindSQL.getSql(), bindSQL.getBindArgs());
+	}
+	
+	/**
+	 * 根据SQL进行查询
+	 * @param sql
+	 * @return
+	 */
+	public Cursor query(String sql) {
+		return query(sql, null);
+	}
+	
+	/**
+	 * 执行绑定语句
+	 * 运行一个预置的SQL语句,返回带游标的数据集(与query的语句最大的区别 = 防止SQL注入)
+	 * @param sql								sql语句
+	 * @param whereArgs							搜索条件
+	 * @return
+	 */
+	public Cursor query(String sql, String[] whereArgs) {
+		DBLog.debug("{SQL:" + sql + ",PARAMS:" + whereArgs + "}");
+		return this.mSQLiteDataBase.rawQuery(sql, whereArgs); 
+	}
+
+	/**
+	 * 根据BindSQL查询
+	 * @param bindSQL
+	 * @return
+	 */
+	public Cursor query(BindSQL bindSQL) {
+		return query(bindSQL.getSql(), (String[])bindSQL.getBindArgs());
+	}
+
+	@Deprecated
+	public Cursor query(boolean distinct, String table, String[] columns,
+						String selection, String[] selectionArgs, String groupBy,
+						String having, String orderBy, String limit) {
+		// 查询指定的数据表返回一个带游标的数据集。
+		// 各参数说明:
+		// table:表名称
+		// colums:列名称数组
+		// selection:条件子句,相当于where
+		// selectionArgs:条件语句的参数数组
+		// groupBy:分组
+		// having:分组条件
+		// orderBy:排序类
+		// limit:分页查询的限制
+		return this.mSQLiteDataBase.query(distinct, table, columns, selection,
+				selectionArgs, groupBy, having, orderBy, limit);
+	}
+
+}

+ 382 - 0
VideoSqlHelper/src/main/java/com/yc/database/sql/SQLBuilder.java

@@ -0,0 +1,382 @@
+package com.yc.database.sql;
+
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+
+import com.yc.database.bean.BindSQL;
+import com.yc.database.bean.EntityTable;
+import com.yc.database.bean.Property;
+import com.yc.database.manager.EntityTableManager;
+import com.yc.database.manager.FieldTypeManager;
+import com.yc.database.utils.ValueUtil;
+
+
+/**
+ * <pre>
+ *     @author yangchong
+ *     email  : yangchong211@163.com
+ *     time  : 2017/8/6
+ *     desc  : SQL语句构造器
+ *     revise:
+ * </pre>
+ */
+public class SQLBuilder {
+	
+	/**
+	 * 获取当前实体类的表名
+	 * @param mClass
+	 * @return
+	 */
+	public static String getTableName(Class<?> mClass) {
+		com.yc.database.annotation.Table anno = mClass.getAnnotation(com.yc.database.annotation.Table.class);
+		if(anno == null || ValueUtil.isEmpty(anno.name())) {
+			return mClass.getSimpleName();
+		}
+		return anno.name();
+	}
+	
+	/**
+	 * 根据实体对象构造建表语句
+	 * @param entity
+	 * @return
+	 */
+	public static String getCreateTableSQL(EntityTable entity) {
+		StringBuilder createTableSql = new StringBuilder();
+		createTableSql.append("CREATE TABLE IF NOT EXISTS ");
+		createTableSql.append(entity.getTableName());//表名
+		createTableSql.append("(");
+		createTableSql.append(entity.getPrimaryKey().getColumn());//主键字段
+		createTableSql.append(" ");
+		createTableSql.append(FieldTypeManager.getColumnTypeValue(entity.getPrimaryKey().getField()));//主键字段类型
+		createTableSql.append(" PRIMARY KEY ");//主键
+		if(entity.getPrimaryKey().isAutoGenerate()) {
+			createTableSql.append(" AUTOINCREMENT ");
+		}
+		LinkedHashMap<String, Property> columnMap = entity.getColumnMap();
+		for(String key : columnMap.keySet()) {
+			createTableSql.append(",");
+			createTableSql.append(columnMap.get(key).getColumn()).append(" ");//字段名
+			createTableSql.append(FieldTypeManager.getColumnTypeValue(columnMap.get(key).getField()));//字段类型
+			if(!ValueUtil.isEmpty(columnMap.get(key).getDefaultValue())) {
+				createTableSql.append(" ").append("DEFAULT ").append(columnMap.get(key).getDefaultValue());
+			}
+		}
+		createTableSql.append(")");
+		return createTableSql.toString();
+	}
+	
+	/**
+	 * 检查表是否存在语句
+	 * @param tableName
+	 * @return
+	 */
+	public static String getCheckTableExistSQL(String tableName) {
+		return "SELECT COUNT(*) TOTALCOUNT FROM SQLITE_MASTER WHERE UPPER(TYPE) ='TABLE' AND NAME = '" + tableName + "'";
+	}
+	
+	/**
+	 * 查询某个表中所有的字段
+	 * @param tableName
+	 * @return
+	 */
+	public static String getTableAllColumnSQL(String tableName) {
+		return "SELECT * FROM "  + tableName + " LIMIT 0";
+	}
+	
+	/**
+	 * 更新表结构SQL(SQLite不支持删除列)
+	 * @param tableName
+	 * @param property
+	 * @return
+	 */
+	public static String getAlterTableSQL(String tableName, Property property) {
+		return "ALTER TABLE " + tableName + 
+			   " ADD COLUMN " + property.getColumn() + " " + FieldTypeManager.getColumnTypeValue(property.getField()) +
+			   (ValueUtil.isEmpty(property.getDefaultValue()) ? "" : " DEFAULT " + property.getDefaultValue());
+	}
+	
+	/**
+	 * 构造插入语句
+	 * @param entity	要插入的实体对象
+	 * @return
+	 */
+	public static <T> BindSQL getInsertSQL(T entity) {
+		EntityTable entityTable = EntityTableManager.getEntityTable(entity);
+		
+		boolean isAutoIncrement = entityTable.getPrimaryKey().isAutoGenerate();
+		
+		String tableName = entityTable.getTableName();
+		Collection<Property> propertys = entityTable.getColumnMap().values();
+		Object[] bindArgs = null;
+		if(isAutoIncrement) {//如果为自增长主键,则在构造插入语句的时候不需要构造主键列,数据库会自动为自增长主键设置值
+			bindArgs = new Object[propertys.size()];
+		} else {
+			bindArgs = new Object[propertys.size() + 1];
+		}
+
+		StringBuilder sqlBuilder = new StringBuilder();
+		StringBuilder argsBuidler = new StringBuilder();
+		int i = 0;
+		
+		sqlBuilder.append("INSERT INTO ").append(tableName).append("(");
+		argsBuidler.append(" VALUES(");
+		
+		if(!isAutoIncrement) {//主键不是自增长列,需要自己设置主键值
+			sqlBuilder.append(entityTable.getPrimaryKey().getColumn()).append(",");//主键
+			argsBuidler.append("?").append(",");
+			if(ValueUtil.isEmpty(entityTable.getPrimaryKey().getValue(entity))) {//判断主键值是否为空
+				throw new IllegalArgumentException("非自增长主键必须手动设置主键值");
+			}
+			bindArgs[i++] = entityTable.getPrimaryKey().getValue(entity);
+		}
+		
+		Iterator<Property> iterator = propertys.iterator();
+		Property property;
+		while(iterator.hasNext()) {
+			property = iterator.next();
+			
+			sqlBuilder.append(property.getColumn());
+			argsBuidler.append("?");
+			
+			bindArgs[i++] = getPropertyValue(property, entity);
+			
+			sqlBuilder.append(",");
+			argsBuidler.append(",");
+		}
+		sqlBuilder.deleteCharAt(sqlBuilder.length() - 1);
+		argsBuidler.deleteCharAt(argsBuidler.length() - 1);
+		sqlBuilder.append(")");
+		argsBuidler.append(")");
+		sqlBuilder.append(argsBuidler);
+		
+		BindSQL bindSQL = new BindSQL(sqlBuilder.toString());
+		bindSQL.setBindArgs(bindArgs);
+		return bindSQL;
+	}
+	
+	private static <T> String getPropertyValue(Property property, T entity) {
+		String value = null;
+		Object obj = property.getValue(entity);//此值的类型为多种基本类型,如果为int、long  直接赋值给value会运行报错
+		switch(FieldTypeManager.getFieldType(property.getField())) {
+			case FieldTypeManager.BASE_TYPE_DOUBLE:
+			case FieldTypeManager.BASE_TYPE_FLOAT:
+			case FieldTypeManager.BASE_TYPE_INT:
+			case FieldTypeManager.BASE_TYPE_LONG:
+			case FieldTypeManager.BASE_TYPE_SHORT:
+				if(Double.parseDouble(obj.toString()) == 0 && !ValueUtil.isEmpty(property.getDefaultValue())) {
+					value = property.getDefaultValue();
+				} else {
+					value = obj.toString();
+				}
+				break;
+			case FieldTypeManager.BASE_TYPE_BOOLEAN:
+				if(Boolean.parseBoolean(obj.toString()) == false && !ValueUtil.isEmpty(property.getDefaultValue())) {
+					value = property.getDefaultValue();
+				} else {
+					value = obj.toString();
+				}
+				break;
+			case FieldTypeManager.BASE_TYPE_CHAR:
+			case FieldTypeManager.BASE_TYPE_STRING:
+			case FieldTypeManager.BASE_TYPE_DATE:
+				if(!ValueUtil.isEmpty(obj)) {
+					value = obj.toString();
+				} else {
+					if(!ValueUtil.isEmpty(property.getDefaultValue())) {
+						value = property.getDefaultValue();
+					}
+				}
+				break;
+		}
+		return value;
+	}
+	
+	/**
+	 * 构造删除语句
+	 * @param entity	要删除的实体
+	 * @return
+	 */
+	public static <T> BindSQL getDeleteSQL(T entity) {
+		EntityTable entityTable = EntityTableManager.getEntityTable(entity);
+		StringBuilder sqlBuilder = new StringBuilder();
+		StringBuilder argsBuilder = new StringBuilder();
+		
+		sqlBuilder.append("DELETE FROM ").append(entityTable.getTableName());
+		sqlBuilder.append(" WHERE ").append(entityTable.getPrimaryKey().getColumn()).append(" = ?");
+		if(ValueUtil.isEmpty(entityTable.getPrimaryKey().getValue(entity))) {
+			throw new IllegalArgumentException("未设置要删除实体的主键");
+		}
+		argsBuilder.append(entityTable.getPrimaryKey().getValue(entity));
+		
+		return new BindSQL(sqlBuilder.toString(), new String[]{argsBuilder.toString()});
+	}
+	
+	/**
+	 * 构造要删除的语句
+	 * @param mClass				要删除的实体类
+	 * @param primaryKeyValue		要删除的实体的主键,主键为空,则删除所有的数据
+	 * @return
+	 */
+	public static BindSQL getDeleteSQL(Class<?> mClass, String primaryKeyValue) {
+		EntityTable entityTable = EntityTableManager.getEntityTable(mClass);
+		StringBuilder sqlBuilder = new StringBuilder();
+		
+		sqlBuilder.append("DELETE FROM ").append(entityTable.getTableName());
+		String[] bindArgs = null;
+		if(!ValueUtil.isEmpty(primaryKeyValue)) {
+			sqlBuilder.append(" WHERE ").append(entityTable.getPrimaryKey().getColumn()).append(" = ?");
+			bindArgs = new String[]{primaryKeyValue};
+		}
+		
+		return new BindSQL(sqlBuilder.toString(), bindArgs);
+	}
+	
+	/**
+	 * 构造更新数据
+	 * @param entity
+	 * @return
+	 */
+	public static <T> BindSQL getUpdateSQL(T entity) {
+		EntityTable entityTable = EntityTableManager.getEntityTable(entity);
+		
+		if(ValueUtil.isEmpty(entityTable.getPrimaryKey().getValue(entity))) {
+			throw new IllegalArgumentException("未设置要删除实体的主键");
+		}
+		
+		StringBuilder sqlBuilder = new StringBuilder();
+		
+		sqlBuilder.append("UPDATE ");
+		sqlBuilder.append(entityTable.getTableName());
+		sqlBuilder.append(" SET ");
+		
+		Collection<Property> propertys = entityTable.getColumnMap().values();
+		Object[] bindArgs = new Object[propertys.size() + 1];
+		int i = 0;
+		
+		Iterator<Property> iterator = propertys.iterator();
+		Property property;
+		while(iterator.hasNext()) {
+			property = iterator.next();
+			
+			sqlBuilder.append(property.getColumn());
+			sqlBuilder.append(" = ?,");
+			bindArgs[i++] = property.getValue(entity);
+		}
+		sqlBuilder.deleteCharAt(sqlBuilder.length() - 1);
+		sqlBuilder.append(" WHERE ");
+		sqlBuilder.append(entityTable.getPrimaryKey().getColumn());
+		sqlBuilder.append(" = ?");
+		bindArgs[i++] = entityTable.getPrimaryKey().getValue(entity);
+		
+		return new BindSQL(sqlBuilder.toString(), bindArgs);
+	}
+	
+	/**
+	 * 查询指定实体的全部数据
+	 * @param mClass	要查询的实体类
+	 * @return
+	 */
+	public static String getQuerySQL(Class<?> mClass) {
+		String tableName = EntityTableManager.getEntityTable(mClass).getTableName();
+		return "SELECT * FROM " + tableName;
+	}
+	
+	/**
+	 * 查询指定实体的全部数据
+	 * @param mClass	要查询的实体类
+	 * @return
+	 */
+	public static BindSQL getQuerySQLById(Class<?> mClass, String primaryValue) {
+		EntityTable entityTable = EntityTableManager.getEntityTable(mClass);
+		StringBuilder sqlBuilder = new StringBuilder();
+		sqlBuilder.append("SELECT * FROM ");
+		sqlBuilder.append(entityTable.getTableName());
+		sqlBuilder.append(" WHERE ");
+		sqlBuilder.append(entityTable.getPrimaryKey().getColumn());
+		sqlBuilder.append(" = ?");
+		return new BindSQL(sqlBuilder.toString(), new String[]{primaryValue});
+	}
+	
+	/**
+	 * 查询指定实体的全部数据
+	 * @param mClass		要查询的实体类
+	 * @param whereClause	where子句
+	 * @param whereArgs		where字句绑定参数
+	 * @return
+	 */
+	public static BindSQL getQuerySQL(Class<?> mClass, String whereClause, String[] whereArgs) {
+		EntityTable entityTable = EntityTableManager.getEntityTable(mClass);
+		StringBuilder sqlBuilder = new StringBuilder();
+		sqlBuilder.append("SELECT * FROM ");
+		sqlBuilder.append(entityTable.getTableName());
+		sqlBuilder.append(" WHERE ");
+		sqlBuilder.append(whereClause);
+		return new BindSQL(sqlBuilder.toString(), whereArgs);
+	}
+	
+	/**
+	 * 查询实体类数据总条数
+	 * @param mClass
+	 * @return
+	 */
+	public static BindSQL getTotalSQL(Class<?> mClass, String whereClause, String[] whereArgs) {
+		String tableName = EntityTableManager.getEntityTable(mClass).getTableName();
+		String sql = "SELECT COUNT(*) TOTALCOUNT FROM " + tableName;
+		if(!ValueUtil.isEmpty(whereClause)) {
+			sql = sql + " WHERE " + whereClause;
+		}
+		return new BindSQL(sql, whereArgs);
+	}
+	
+	/**
+	 * 获取分页查询语句
+	 * @param mClass		要查询的实体类
+	 * @param curPage		查询的当前页码
+	 * @param pageSize		每页数据数
+	 * @return
+	 */
+	public static BindSQL getQueryPageSQL(Class<?> mClass, int curPage, int pageSize) {
+		EntityTable entityTable = EntityTableManager.getEntityTable(mClass);
+		StringBuilder sqlBuilder = new StringBuilder();
+		sqlBuilder.append("SELECT * FROM ");
+		sqlBuilder.append(entityTable.getTableName());
+		sqlBuilder.append(" LIMIT ? OFFSET ? * ? ");
+		String cur = String.valueOf(curPage - 1);
+		String size = String.valueOf(pageSize);
+		return new BindSQL(sqlBuilder.toString(), new String[]{size, cur, size});
+	}
+	
+	/**
+	 * 获取分页查询语句
+	 * @param mClass		要查询的实体类
+	 * @param whereClause	查询where子句
+	 * @param whereArgs		where子句参数
+	 * @param curPage		查询的当前页码
+	 * @param pageSize		每页数据数
+	 * @return
+	 */
+	public static BindSQL getQueryPageSQL(Class<?> mClass, String whereClause, String[] whereArgs, int curPage, int pageSize) {
+		EntityTable entityTable = EntityTableManager.getEntityTable(mClass);
+		StringBuilder sqlBuilder = new StringBuilder();
+		sqlBuilder.append("SELECT * FROM ");
+		sqlBuilder.append(entityTable.getTableName());
+		if(!ValueUtil.isEmpty(whereClause)) {
+			sqlBuilder.append(" WHERE ");
+			sqlBuilder.append(whereClause);
+		}
+		sqlBuilder.append(" LIMIT ? OFFSET ? * ? ");
+		String cur = String.valueOf(curPage - 1);
+		String size = String.valueOf(pageSize);
+		
+		int length = whereArgs == null ? 0 : whereArgs.length;
+		String[] newWhereArgs = new String[length + 3];
+		if(length > 0) {
+			System.arraycopy(whereArgs, 0, newWhereArgs, 0, whereArgs.length);
+		}
+		newWhereArgs[length] = size;
+		newWhereArgs[length + 1] = cur;
+		newWhereArgs[length + 2] = size;
+		return new BindSQL(sqlBuilder.toString(), newWhereArgs);
+	}
+}

+ 122 - 0
VideoSqlHelper/src/main/java/com/yc/database/sql/SQLiteContext.java

@@ -0,0 +1,122 @@
+package com.yc.database.sql;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.database.DatabaseErrorHandler;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteDatabase.CursorFactory;
+import android.os.Environment;
+
+import java.io.File;
+
+import com.yc.database.utils.ValueUtil;
+
+
+/**
+ * <pre>
+ *     @author yangchong
+ *     email  : yangchong211@163.com
+ *     time  : 2017/8/6
+ *     desc  : 自定义数据库创建容器(设置数据库创建的自定义目录)
+ *     revise:
+ * </pre>
+ */
+public class SQLiteContext extends ContextWrapper {
+
+	private SQLiteDBConfig config;
+	
+	public SQLiteContext(Context base, SQLiteDBConfig config) {
+		super(base);
+		this.config = config;
+	}
+	
+	@Override
+	public File getDatabasePath(String name) {
+		if (ValueUtil.isEmpty(config.getDbDirectoryPath())) {
+			return super.getDatabasePath(name);
+		}
+		String phoneRootPath = getPhoneRootPath();
+		String[] files = null;
+		if(phoneRootPath.startsWith("/")) {
+			files = phoneRootPath.substring(1).split("/");
+		} else {
+			files = phoneRootPath.split("/");
+		}
+		boolean flag = false;
+		for(int i = 0; i < files.length; i++) {
+			if(config.getDbDirectoryPath().contains(files[i])) {
+				flag = true;
+				break;
+			}
+		}
+		String dbPath = config.getDbDirectoryPath();
+		if(flag == false) {
+			dbPath = phoneRootPath + config.getDbDirectoryPath();
+		}
+		if(!config.getDbDirectoryPath().endsWith("/")) {
+			dbPath = dbPath + "/";
+		}
+		dbPath = dbPath + config.getDbName();
+		makeParentDir(dbPath);
+		return new File(dbPath);
+	}
+	
+	/**
+	 * 判断其父目录是否存在,不存在则创建
+	 * @param path
+	 */
+	private void makeParentDir(String path) {
+		String parentPath = getParentPath(path);
+		File file = new File(parentPath);
+		if(!file.exists()) {
+			makeParentDir(parentPath);
+			file.mkdir();
+		}
+	}
+	
+	/**
+	 * 获取父级目录
+	 * @param path
+	 * @return
+	 */
+	public static String getParentPath(String path) {
+		if (path.equals("/")) {
+			return path;
+		}
+		if (path.endsWith("/")) {
+			path = path.substring(0, path.length() - 1);
+		}
+		path = path.substring(0, path.lastIndexOf("/"));
+		return path.equals("") ? "/" : path;
+	}
+	
+	@Override
+	public SQLiteDatabase openOrCreateDatabase(String name, int mode, CursorFactory factory) {
+		if (ValueUtil.isEmpty(config.getDbDirectoryPath())) {
+			return super.openOrCreateDatabase(name, mode, factory);
+		}
+		return SQLiteDatabase.openOrCreateDatabase(getDatabasePath(name), null);
+	}
+	
+	@SuppressLint("NewApi")
+	@Override
+	public SQLiteDatabase openOrCreateDatabase(String name, int mode,
+                                               CursorFactory factory, DatabaseErrorHandler errorHandler) {
+		if (ValueUtil.isEmpty(config.getDbDirectoryPath())) {
+			return super.openOrCreateDatabase(name, mode, factory, errorHandler);
+		}
+		return SQLiteDatabase.openOrCreateDatabase(getDatabasePath(name), null);
+	}
+	
+	/**
+	 * 获取手机根目录
+	 * @return
+	 */
+	public String getPhoneRootPath() {
+		if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
+			return Environment.getExternalStorageDirectory().getPath();
+		}
+		return Environment.getDataDirectory().getAbsolutePath();
+	}
+}

+ 462 - 0
VideoSqlHelper/src/main/java/com/yc/database/sql/SQLiteDB.java

@@ -0,0 +1,462 @@
+package com.yc.database.sql;
+
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+
+import java.sql.SQLException;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+
+import com.yc.database.bean.BindSQL;
+import com.yc.database.bean.EntityTable;
+import com.yc.database.bean.PrimaryKey;
+import com.yc.database.manager.EntityTableManager;
+import com.yc.database.manager.SQLExecuteManager;
+import com.yc.database.utils.CursorUtil;
+import com.yc.database.utils.ValueUtil;
+
+
+/**
+ * <pre>
+ *     @author yangchong
+ *     email  : yangchong211@163.com
+ *     time  : 2017/8/6
+ *     desc  : 数据库操作类
+ *     revise: 注:所有操作的实体必须实现无参构造函数
+ * </pre>
+ */
+public class SQLiteDB {
+
+	/**
+	 * 数据库配置
+	 */
+	private SQLiteDBConfig mConfig;
+	/**
+	 * 数据库操作类
+	 */
+	private SQLiteDatabase mDB;
+	/**
+	 * SQL语句执行管理器
+	 */
+	private SQLExecuteManager mSQLExecuteManager;
+	
+	public SQLExecuteManager getSQLExecuteManager() {
+		return mSQLExecuteManager;
+	}
+
+	public SQLiteDBConfig getConfig() {
+		return mConfig;
+	}
+	
+	public SQLiteDB(SQLiteDBConfig mConfig) {
+		super();
+		this.mConfig = mConfig;
+		createDB();
+	}
+	
+	/**
+	 * 创建数据库
+	 */
+	private void createDB() {
+		SQLiteHelper sqLiteHelper = new SQLiteHelper(mConfig);
+		mDB = sqLiteHelper.getWritableDatabase();
+		if(mDB == null) {
+			throw new NullPointerException("创建数据库对象失败");
+		}
+		mSQLExecuteManager = new SQLExecuteManager(mDB);
+	}
+	
+	/**
+	 * 关闭当前数据库
+	 */
+	public void close() {
+		this.mDB.close();
+	}
+	
+	/**
+	 * 判断当前数据库是否打开
+	 * @return
+	 */
+	public boolean isOpen() {
+		return this.mDB.isOpen();
+	}
+	
+	/**
+	 * 重新打开数据库
+	 */
+	public void reOpen() {
+		if(!isOpen()) {
+			createDB();
+		}
+	}
+
+	/**
+	 * 保存一个实体
+	 * 当主键设置为自增长时,手动设置的主键值不会起作用,同时会自动加保存之后最新的主键值填充到对象中,并返回
+	 * @param entity							对象
+	 * @return
+	 */
+	public <T> long save(T entity) {
+		if (entity==null){
+			return -1;
+		}
+		EntityTableManager.checkOrCreateTable(mSQLExecuteManager, entity);
+		BindSQL insertSQL = SQLBuilder.getInsertSQL(entity);
+		long rowid = mSQLExecuteManager.insert(insertSQL);
+		EntityTable entityTable = EntityTableManager.getEntityTable(entity);
+		PrimaryKey key = entityTable.getPrimaryKey();
+		if(key.isAutoGenerate()) {
+			key.getField().setAccessible(true);
+			try {
+				key.setValue(entity, rowid);
+			} catch (Exception e) {
+				e.printStackTrace();
+			}
+		}
+		return rowid;
+	}
+	
+	/**
+	 * 保存集合实体
+	 * @param collection						集合
+	 * @return									如果集合为空或者批量保存失败则返回-1,保存成功返回集合大小
+	 */
+	public <T> long save(Collection<T> collection) {
+		long rowId = -1;
+		if(ValueUtil.isEmpty(collection)) {
+			return rowId;
+		}
+		try {
+			mSQLExecuteManager.beginTransaction();
+			Iterator<T> iterator = collection.iterator();
+			while(iterator.hasNext()) {
+				rowId = save(iterator.next());
+				if(rowId == -1) {
+					throw new SQLException("删除实体失败");
+				}
+			}
+			mSQLExecuteManager.successTransaction();
+			rowId = collection.size();
+		} catch (SQLException e) {
+			e.printStackTrace();
+			rowId = -1;
+		} finally {
+			mSQLExecuteManager.endTransaction();
+		}
+		return rowId;
+	}
+	
+	/**
+	 * 删除指定实体(根据主键删除)
+	 * @param entity	要删除的实体
+	 */
+	public <T> void delete(T entity) {
+		if(ValueUtil.isEmpty(entity)) {
+			return ;
+		}
+		EntityTableManager.checkOrCreateTable(mSQLExecuteManager, entity);
+		mSQLExecuteManager.delete(SQLBuilder.getDeleteSQL(entity));
+	}
+	
+	/**
+	 * 删除集合中的实体(有事务控制),每个实体根据主键删除
+	 * @param collection	要删除的实体集合
+	 */
+	public <T> void delete(Collection<T> collection) {
+		if(ValueUtil.isEmpty(collection)) {
+			return;
+		}
+		
+		try {
+			Iterator<T> iterator = collection.iterator();
+			this.mSQLExecuteManager.beginTransaction();
+			while(iterator.hasNext()) {
+				delete(iterator.next());
+			}
+			this.mSQLExecuteManager.successTransaction();
+		} finally {
+			this.mSQLExecuteManager.endTransaction();
+		}
+	}
+	
+	/**
+	 * 删除实体类中指定主键的实体
+	 * @param mClass				要删除的实体类
+	 * @param primaryKeyValue		要删除的实体的主键值
+	 */
+	public void delete(Class<?> mClass, String primaryKeyValue) {
+		if(ValueUtil.isEmpty(primaryKeyValue)) {
+			throw new IllegalArgumentException("要删除的实体的主键不能为空");
+		}
+		EntityTableManager.checkOrCreateTable(mSQLExecuteManager, mClass);
+		mSQLExecuteManager.delete(SQLBuilder.getDeleteSQL(mClass, primaryKeyValue));
+	}
+	
+	/**
+	 * 根据指定条件删除指定的实体
+	 * @param mClass		要删除的实体类
+	 * @param whereClause	where后面的条件句(delete from XXX where XXX),参数使用占位符
+	 * @param whereArgs		占位符参数
+	 */
+	public void delete(Class<?> mClass, String whereClause, String[] whereArgs) {
+		EntityTableManager.checkOrCreateTable(mSQLExecuteManager, mClass);
+		EntityTable entityTable = EntityTableManager.getEntityTable(mClass);
+		delete(entityTable.getTableName(), whereClause, whereArgs);
+	}
+	
+	/**
+	 * 根据where条件句删除相关实体
+	 * @param tableName			要删除的数据表
+	 * @param whereClause		where后面的条件句(delete from XXX where XXX),参数使用占位符
+	 * @param whereArgs			占位符参数
+	 * @return
+	 */
+	public void delete(String tableName, String whereClause, String[] whereArgs) {
+		mSQLExecuteManager.delete(tableName, whereClause, whereArgs);
+	}
+	
+	/**
+	 * 删除,表名不能使用占位符
+	 * @param sql			删除语句(参数使用占位符)
+	 * @param bindArgs		占位符参数
+	 */
+	public void delete(String sql, String[] bindArgs) {
+		mSQLExecuteManager.updateOrDelete(sql, bindArgs);
+	}
+	
+	/**
+	 * 删除实体类所有数据
+	 * @param mClass
+	 */
+	public void deleteAll(Class<?> mClass) {
+		EntityTableManager.checkOrCreateTable(mSQLExecuteManager, mClass);
+		mSQLExecuteManager.delete(SQLBuilder.getDeleteSQL(mClass, null));
+	}
+	
+	/**
+	 * 更新指定实体(必须设置主键,根据主键更新)
+	 * @param entity
+	 * @return
+	 */
+	public <T> void update(T entity) {
+		EntityTableManager.checkOrCreateTable(mSQLExecuteManager, entity);
+		mSQLExecuteManager.update(SQLBuilder.getUpdateSQL(entity));
+	}
+	
+	/**
+	 * 更新指定集合数据(每个实体必须设置主键,根据主键更新)
+	 * @param collection
+	 */
+	public <T> void update(Collection<T> collection) {
+		if(ValueUtil.isEmpty(collection)) {
+			return;
+		}
+		try {
+			Iterator<T> iterator = collection.iterator();
+			this.mSQLExecuteManager.beginTransaction();
+			while(iterator.hasNext()) {
+				update(iterator.next());
+			}
+			this.mSQLExecuteManager.successTransaction();
+		} finally {
+			this.mSQLExecuteManager.endTransaction();
+		}
+	}
+	
+	/**
+	 * 更新
+	 * @param sql
+	 * @param bindArgs
+	 */
+	public void update(String sql, String[] bindArgs) {
+		mSQLExecuteManager.updateOrDelete(sql, bindArgs);
+	}
+	
+	/**
+	 * 查询实体类全部数据
+	 * @param mClass	要查询的实体类
+	 * @return			实体列表
+	 */
+	public <T> List<T> queryAll(Class<T> mClass) {
+		EntityTableManager.checkOrCreateTable(mSQLExecuteManager, mClass);
+		Cursor cursor = mSQLExecuteManager.query(SQLBuilder.getQuerySQL(mClass));
+		return CursorUtil.parseCursor(cursor, mClass);
+	}
+	
+	/**
+	 * 根据主键查询指定实体
+	 * @param primaryKeyValue
+	 * @return
+	 */
+	public <T> T query(Class<T> mClass, String primaryKeyValue) {
+		EntityTableManager.checkOrCreateTable(mSQLExecuteManager, mClass);
+		Cursor cursor = mSQLExecuteManager.query(SQLBuilder.getQuerySQLById(mClass, primaryKeyValue));
+		return CursorUtil.parseCursorOneResult(cursor, mClass);
+	}
+	
+	/**
+	 * 根据条件查询实体类
+	 * @param mClass		查询的实体类
+	 * @param whereClause	查询条件where子句
+	 * @param whereArgs		where子句参数
+	 * @return
+	 */
+	public <T> List<T> query(Class<T> mClass, String whereClause, String[] whereArgs) {
+		EntityTableManager.checkOrCreateTable(mSQLExecuteManager, mClass);
+		Cursor cursor = mSQLExecuteManager.query(SQLBuilder.getQuerySQL(mClass, whereClause, whereArgs));
+		return CursorUtil.parseCursor(cursor, mClass);
+	}
+	
+	/**
+	 * 根据条件查询符合条件的第一条实体类
+	 * @param mClass		查询的实体类
+	 * @param whereClause	查询条件where子句
+	 * @param whereArgs		where子句参数
+	 * @return	存在返回第一条实体类,不存在返回null
+	 */
+	public <T> T queryOne(Class<T> mClass, String whereClause, String[] whereArgs) {
+		EntityTableManager.checkOrCreateTable(mSQLExecuteManager, mClass);
+		Cursor cursor = mSQLExecuteManager.query(SQLBuilder.getQuerySQL(mClass, whereClause, whereArgs));
+		List<T> list = CursorUtil.parseCursor(cursor, mClass);
+		return list.size() > 0 ? list.get(0) : null;
+	}
+	
+	/**
+	 * 根据SQL语句查询实体类
+	 * @param mClass		查询的实体类
+	 * @param sql			查询条件where子句
+	 * @param whereArgs		where子句参数
+	 * @return
+	 */
+	public <T> List<T> queryBySQL(Class<T> mClass, String sql, String[] whereArgs) {
+		EntityTableManager.checkOrCreateTable(mSQLExecuteManager, mClass);
+		Cursor cursor = mSQLExecuteManager.query(new BindSQL(sql, whereArgs));
+		return CursorUtil.parseCursor(cursor, mClass);
+	}
+	
+	/**
+	 * 分页查询
+	 * @param mClass	查询实体类
+	 * @param curPage	当前页码
+	 * @param pageSize	每页数据条数
+	 * @return
+	 */
+	public <T> List<T> queryPage(Class<T> mClass, int curPage, int pageSize) {
+		EntityTableManager.checkOrCreateTable(mSQLExecuteManager, mClass);
+		Cursor cursor = mSQLExecuteManager.query(SQLBuilder.getQueryPageSQL(mClass, curPage, pageSize));
+		return CursorUtil.parseCursor(cursor, mClass);
+	}
+	
+	/**
+	 * 分页查询
+	 * @param mClass	查询实体类,返回实体类型
+	 * @param curPage	当前页码
+	 * @param pageSize	每页数据条数
+	 * @return
+	 */
+	/**
+	 * 分页查询
+	 * @param mClass				查询实体类,返回实体类型
+	 * @param whereClause			查询语句
+	 * @param whereArgs				查询语句中的参数
+	 * @param curPage				当前页码
+	 * @param pageSize				每页数据条数
+	 * @return
+	 */
+	public <T> List<T> queryPage(Class<T> mClass, String whereClause, String[] whereArgs, int curPage, int pageSize) {
+		EntityTableManager.checkOrCreateTable(mSQLExecuteManager, mClass);
+		Cursor cursor = mSQLExecuteManager.query(SQLBuilder.getQueryPageSQL(mClass, whereClause, whereArgs, curPage, pageSize));
+		return CursorUtil.parseCursor(cursor, mClass);
+	}
+	
+	/**
+	 * 查询实体类的总数据条数
+	 * @param mClass	查询实体类
+	 * @return
+	 */
+	public long queryTotal(Class<?> mClass) {
+		EntityTableManager.checkOrCreateTable(mSQLExecuteManager, mClass);
+		return queryTotal(mClass, null, null);
+	}
+	
+	/**
+	 * 查询实体类指定条件下的数据总数
+	 * @param mClass			查询实体类
+	 * @param whereClause		查询条件
+	 * @param whereArgs			查询条件中的占位符参数
+	 * @return
+	 */
+	public long queryTotal(Class<?> mClass, String whereClause, String[] whereArgs) {
+		EntityTableManager.checkOrCreateTable(mSQLExecuteManager, mClass);
+		Cursor cursor = mSQLExecuteManager.query(SQLBuilder.getTotalSQL(mClass, whereClause, whereArgs));
+		return CursorUtil.parseCursorTotal(cursor);
+	}
+	
+	/**
+	 * 根据SQL语句查询数据条数(解析结果为第一列值)
+	 * @param sql			查询SQL
+	 * @param bindArgs		占位符参数值
+	 * @return
+	 */
+	public long queryTotal(String sql, String[] bindArgs) {
+		Cursor cursor = mSQLExecuteManager.query(sql, bindArgs);
+		return CursorUtil.parseCursorTotal(cursor);
+	}
+	
+	/**
+	 * 查询指定实体类中是否存在指定主键值的实体对象
+	 * @param mClass			查询实体类
+	 * @param primaryKeyValue	实体主键值
+	 * @return	存在返回true,不存在返回false
+	 */
+	public boolean queryIfExist(Class<?> mClass, String primaryKeyValue) {
+		EntityTableManager.checkOrCreateTable(mSQLExecuteManager, mClass);
+		EntityTable entityTable = EntityTableManager.getEntityTable(mClass);
+		String whereClause = entityTable.getPrimaryKey().getColumn() + "=?";
+		String[] whereArgs = {primaryKeyValue};
+		long count = queryTotal(mClass, whereClause, whereArgs);
+		return count > 0 ? true : false;
+	}
+	
+	/**
+	 * 根据查询条件判断是否存在指定的数据
+	 * @param mClass		查询实体类
+	 * @param whereClause	查询条件
+	 * @param whereArgs		查询参数
+	 * @return				存在返回true,不存在返回false
+	 */
+	public boolean queryIfExist(Class<?> mClass, String whereClause, String[] whereArgs) {
+		long count = queryTotal(mClass, whereClause, whereArgs);
+		return count > 0 ? true : false;
+	}
+	
+	/**
+	 * 根据SQL语句查询
+	 * @param sql
+	 * @param bindArgs
+	 * @return
+	 */
+	public Cursor query(String sql, String[] bindArgs) {
+		return mSQLExecuteManager.query(sql, bindArgs);
+	}
+	
+
+	/**
+	 * 根据查询条件查询指定实体的指定字段信息
+	 * @param mClass		要查询的实体
+	 * @param selectCols	要查询的数据库字段,多个查询字段间使用逗号隔开
+	 * @param whereClause	查询条件(无查询条件时,可以传值null)
+	 * @param whereArgs		查询条件参数值
+	 * @return
+	 */
+	public <T> Cursor query(Class<T> mClass, String selectCols, String whereClause, String[] whereArgs) {
+		EntityTableManager.checkOrCreateTable(mSQLExecuteManager, mClass);
+		String tableName = EntityTableManager.getEntityTable(mClass).getTableName();
+		String sql = "SELECT " + selectCols + " FROM " + tableName;
+		if(!ValueUtil.isEmpty(whereClause)) {
+			sql += " WHERE " + whereClause;
+		}
+		return query(sql, whereArgs);
+	}
+}

+ 119 - 0
VideoSqlHelper/src/main/java/com/yc/database/sql/SQLiteDBConfig.java

@@ -0,0 +1,119 @@
+package com.yc.database.sql;
+
+import android.content.Context;
+
+import com.yc.database.listener.InterDBListener;
+import com.yc.database.listener.SimpleDBListener;
+
+
+/**
+ * <pre>
+ *     @author yangchong
+ *     email  : yangchong211@163.com
+ *     time  : 2017/8/6
+ *     desc  : 数据库相关配置
+ *     revise:
+ * </pre>
+ */
+public final class SQLiteDBConfig {
+	/**
+	 * 数据文件的默认存储目录(不包含SDCard目录部分,自动添加),可手动设置多个数据库的默认目录
+	 */
+	public static String DEFAULT_DB_DIRECTORY_PATH = "/TigerDB/";
+	/**
+	 * 默认数据库名,可修改
+	 */
+	public static String DEFAULT_DB_NAME = "Tiger.db";
+	/**
+	 * 默认版本号,可修改
+	 */
+	public static int DEFAULT_VERSION = 1;
+	/**
+	 * 数据库上下文
+	 */
+	private Context mContext;
+	/**
+	 * 当前数据库文件所在目录路径(绝对路径)
+	 */
+	private String mDbDirectoryPath = DEFAULT_DB_DIRECTORY_PATH;
+	/**
+	 * 数据库文件名,默认为:Tiger.db
+	 */
+	private String mDbName = DEFAULT_DB_NAME;
+	/**
+	 * 当前数据库版本号
+	 */
+	private int mVersion = DEFAULT_VERSION;
+	/**
+	 * 数据库监听
+	 */
+	private InterDBListener mDbListener;
+	
+	public SQLiteDBConfig(Context context) {
+		this.mContext = context;
+		mDbListener = new SimpleDBListener();
+	}
+	
+	public SQLiteDBConfig(Context context, String dbName) {
+		super();
+		this.mContext = context;
+		this.mDbName = dbName;
+		mDbListener = new SimpleDBListener();
+	}
+	
+	public SQLiteDBConfig(Context context, String dbDirectoryPath, String dbName) {
+		super();
+		this.mContext = context;
+		this.mDbDirectoryPath = dbDirectoryPath;
+		this.mDbName = dbName;
+		mDbListener = new SimpleDBListener();
+	}
+
+	/**
+	 * 获取数据库所在的上下文
+	 * @return
+	 */
+	public Context getContext() {
+		return mContext;
+	}
+	
+	/**
+	 * 设置数据库所在的上下文
+	 * @param mContext
+	 */
+	public void setContext(Context mContext) {
+		this.mContext = mContext;
+	}
+	
+	public String getDbDirectoryPath() {
+		return mDbDirectoryPath;
+	}
+	
+	public void setDbDirectoryPath(String mDbDirectoryPath) {
+		this.mDbDirectoryPath = mDbDirectoryPath;
+	}
+	
+	public String getDbName() {
+		return mDbName;
+	}
+	
+	public void setDbName(String mDbName) {
+		this.mDbName = mDbName;
+	}
+	
+	public int getVersion() {
+		return mVersion;
+	}
+	
+	public void setVersion(int mVersion) {
+		this.mVersion = mVersion;
+	}
+
+	public InterDBListener getDbListener() {
+		return mDbListener;
+	}
+
+	public void setDbListener(InterDBListener mDbListener) {
+		this.mDbListener = mDbListener;
+	}
+}

+ 73 - 0
VideoSqlHelper/src/main/java/com/yc/database/sql/SQLiteDBFactory.java

@@ -0,0 +1,73 @@
+package com.yc.database.sql;
+
+import android.content.Context;
+
+import java.util.HashMap;
+
+import com.yc.database.utils.ValueUtil;
+
+/**
+ * <pre>
+ *     @author yangchong
+ *     email  : yangchong211@163.com
+ *     time  : 2017/8/6
+ *     desc  : 数据库管理工厂
+ *     revise:
+ * </pre>
+ */
+public class SQLiteDBFactory {
+	/**
+	 * 多个数据库集合对象<dbName, {@link}SQLiteDB>
+	 */
+	private static HashMap<String, SQLiteDB> dbMap = new HashMap<String, SQLiteDB>();
+	
+	/**
+	 * 生成一个名为dnName的数据库,目录为默认目录(参考SQLiteDBConfig里面的目录设置)}
+	 * @param context
+	 * @param dbName		要生成的数据库名称
+	 * @return
+	 */
+	public static SQLiteDB createSQLiteDB(Context context, String dbName) {
+		SQLiteDBConfig confing = new SQLiteDBConfig(context);
+		confing.setDbName(dbName);
+		return createSQLiteDB(confing);
+	}
+	
+	/**
+	 * 在默认目录下生成默认名称的数据库
+	 * @param context
+	 * @return
+	 */
+	public static SQLiteDB createSQLiteDB(Context context) {
+		return createSQLiteDB(new SQLiteDBConfig(context));
+	}
+	
+	/**
+	 * 根据自定义配置生成数据库
+	 * @param config
+	 * @return
+	 */
+	public static SQLiteDB createSQLiteDB(SQLiteDBConfig config) {
+		if(config.getVersion() < 0) {
+			config.setVersion(SQLiteDBConfig.DEFAULT_VERSION);
+		}
+		if(ValueUtil.isEmpty(config.getDbName())) {
+			config.setDbName(SQLiteDBConfig.DEFAULT_DB_NAME);
+		}
+		if(ValueUtil.isEmpty(config.getDbDirectoryPath())) {
+			config.setDbDirectoryPath(SQLiteDBConfig.DEFAULT_DB_DIRECTORY_PATH);
+		}
+		if(!dbMap.containsKey(config.getDbName())) {
+			synchronized (SQLiteDBFactory.class) {
+				if(!dbMap.containsKey(config.getDbName())) {
+					dbMap.put(config.getDbName(), new SQLiteDB(config));
+				}
+			}
+		}
+		SQLiteDB db = dbMap.get(config.getDbName());
+		if(!db.isOpen()) {
+			db.reOpen();
+		}
+		return dbMap.get(config.getDbName());
+	}
+}

+ 136 - 0
VideoSqlHelper/src/main/java/com/yc/database/sql/SQLiteHelper.java

@@ -0,0 +1,136 @@
+package com.yc.database.sql;
+
+import android.content.Context;
+import android.database.DatabaseErrorHandler;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+
+
+/**
+ * <pre>
+ *     @author yangchong
+ *     email  : yangchong211@163.com
+ *     time  : 2017/8/6
+ *     desc  : 继承自SQLiteOpenHelper,扩展实现自定义db的生成路径
+ *     revise: 用作sql存取数据的基础功能库,暂时不想依赖greenDao(插件+100kb库)或者realm(2M)数据库【对于视频播放器库,避免组件体积过大】
+ * </pre>
+ */
+public class SQLiteHelper extends SQLiteOpenHelper {
+
+	/**
+	 * 默认db,解决在onCreate,onUpgrade中执行其他操作数据库操作时出现的异常
+	 * (java.lang.IllegalStateException: getDatabase called recursively)
+	 */
+	private SQLiteDatabase mDefaultSQLiteDatabase = null;
+	/**
+	 * 数据库配置
+	 */
+	private SQLiteDBConfig mConfig;
+	
+	public SQLiteHelper(SQLiteDBConfig config) {
+		this(new SQLiteContext(config.getContext(), config), config.getDbName(), null, config.getVersion());
+		this.mConfig = config;
+	}
+
+	/**
+	 * 创建数据库对象
+	 * @param context							上下文
+	 * @param name								名称
+	 * @param factory							factory
+	 * @param version							版本号
+	 */
+	public SQLiteHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
+		// 参数说明
+		// context:上下文对象
+		// name:数据库名称
+		// param:一个可选的游标工厂(通常是 Null)
+		// version:当前数据库的版本,值必须是整数并且是递增的状态
+
+		// 必须通过super调用父类的构造函数
+		super(context, name, factory, version);
+	}
+
+	/**
+	 * 创建数据库对象
+	 * @param context							上下文
+	 * @param name								名称
+	 * @param factory							factory
+	 * @param version							版本号
+	 * @param errorHandler						errorHandler
+	 */
+	public SQLiteHelper(Context context, String name, SQLiteDatabase.CursorFactory factory,
+						int version, DatabaseErrorHandler errorHandler) {
+		super(context, name, factory, version, errorHandler);
+	}
+
+	/**
+	 * 创建 or 打开 可读/写的数据库(通过 返回的SQLiteDatabase对象 进行操作)
+	 * 对于操作 = “增、删、改(更新)”,需获得 可"读 / 写"的权限:getWritableDatabase()
+	 * @return									SQLiteDatabase对象
+	 */
+	@Override
+	public SQLiteDatabase getWritableDatabase() {
+		if(mDefaultSQLiteDatabase != null) {
+			return mDefaultSQLiteDatabase;
+		}
+		return super.getWritableDatabase();
+	}
+
+	/**
+	 * 创建 or 打开 可读的数据库(通过 返回的SQLiteDatabase对象 进行操作
+	 * 对于操作 = “查询”,需获得 可"读 "的权限getReadableDatabase()
+	 * @return
+	 */
+	@Override
+	public SQLiteDatabase getReadableDatabase() {
+		return super.getReadableDatabase();
+	}
+
+	/**
+	 * 创建数据库调用该方法
+	 * 数据库第1次创建时 则会调用,即 第1次调用 getWritableDatabase() / getReadableDatabase()时调用
+	 * 调用时刻:当数据库第1次创建时调用
+	 * 作用:创建数据库 表 & 初始化数据
+	 * SQLite数据库创建支持的数据类型: 整型数据、字符串类型、日期类型、二进制
+	 * @param db									db数据库
+	 */
+	@Override
+	public void onCreate(SQLiteDatabase db) {
+		this.mDefaultSQLiteDatabase = db;
+		if(mConfig.getDbListener() != null) {
+			mConfig.getDbListener().onDbCreateHandler(db);
+		}
+	}
+
+	/**
+	 * 关闭数据库
+	 */
+	@Override
+	public synchronized void close() {
+		super.close();
+	}
+
+	/**
+	 * 更新数据库
+	 * 数据库升级时自动调用
+	 * 调用时刻:当数据库升级时则自动调用(即 数据库版本 发生变化时)
+	 * 作用:更新数据库表结构
+	 * 注:创建SQLiteOpenHelper子类对象时,必须传入一个version参数,该参数 = 当前数据库版本, 若该版本高于之前版本, 就调用onUpgrade()
+	 * @param db									db数据库
+	 * @param oldVersion							老版本
+	 * @param newVersion							新版本
+	 */
+	@Override
+	public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+		this.mDefaultSQLiteDatabase = db;
+		if(mConfig.getDbListener() != null) {
+			mConfig.getDbListener().onUpgradeHandler(db, oldVersion, newVersion);
+		}
+	}
+
+	@Override
+	public String getDatabaseName() {
+		//获取数据库名称
+		return super.getDatabaseName();
+	}
+}

+ 152 - 0
VideoSqlHelper/src/main/java/com/yc/database/utils/CursorUtil.java

@@ -0,0 +1,152 @@
+package com.yc.database.utils;
+
+import android.database.Cursor;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+
+import com.yc.database.bean.EntityTable;
+import com.yc.database.bean.Property;
+import com.yc.database.manager.EntityTableManager;
+
+
+/**
+ * <pre>
+ *     @author yangchong
+ *     email  : yangchong211@163.com
+ *     time  : 2017/8/6
+ *     desc  : Cursor工具类
+ *     revise:
+ * </pre>
+ */
+public class CursorUtil {
+	/**
+	 * 判断Cursor是否正确,即存在结果集
+	 * @param cursor
+	 * @return
+	 */
+	public static boolean isCursorRight(Cursor cursor) {
+		if(cursor == null || cursor.getCount() <= 0) {
+			return false;
+		}
+		return true;
+	}
+	
+	/**
+	 * 关闭某个Cursor
+	 * @param cursor
+	 */
+	public static void closeCursor(Cursor cursor) {
+		if(cursor != null && !cursor.isClosed()) {
+			cursor.close();
+		}
+		cursor = null;
+	}
+
+	/**
+	 * 解析查询游标结果集为指定实体类列表(解析完成之后会关闭游标)
+	 * @param cursor	游标结果集
+	 * @param mClass	查询实体类
+	 * @return
+	 */
+	public static <T> List<T> parseCursor(Cursor cursor, Class<T> mClass) {
+		List<T> list = new ArrayList<T>();
+		if(!isCursorRight(cursor)) {
+			return list;
+		}
+		long count = cursor.getCount();
+		EntityTable entityTable = EntityTableManager.getEntityTable(mClass);
+		LinkedHashMap<String, Property> propertys = entityTable.getColumnMap();
+		Property primaryKey = entityTable.getPrimaryKey();
+		
+		try { 
+			for(int i = 0; i < count; i++) {
+				cursor.moveToPosition(i);
+				T entity = (T) mClass.newInstance();
+				primaryKey.setValue(entity, cursor);
+				for(String key : propertys.keySet()) {
+					Property property = propertys.get(key);
+					if (property!=null){
+						property.setValue(entity, cursor);
+					}
+				}
+				list.add(entity);
+			}
+		} catch (Exception e) {
+			DBLog.debug("解析查询结果集出错", e);
+			throw new IllegalArgumentException(e);
+		} finally {
+			closeCursor(cursor);
+		}
+		return list;
+	}
+	
+	/**
+	 * 解析查询游标结果集第一条记录(解析完成之后会关闭游标)
+	 * Author: hyl
+	 * Time: 2015-8-21上午10:30:14
+	 * @param cursor	游标结果集
+	 * @param mClass	查询实体类
+	 * @return
+	 */
+	public static <T> T parseCursorOneResult(Cursor cursor, Class<T> mClass) {
+		if(!isCursorRight(cursor)) {
+			return null;
+		}
+		EntityTable entityTable = EntityTableManager.getEntityTable(mClass);
+		LinkedHashMap<String, Property> propertys = entityTable.getColumnMap();
+		Property primaryKey = entityTable.getPrimaryKey();
+		T entity = null;
+		try { 
+			entity = (T) mClass.newInstance();
+			cursor.moveToFirst();
+			primaryKey.setValue(entity, cursor);
+			for(String key : propertys.keySet()) {
+				propertys.get(key).setValue(entity, cursor);
+			}
+		} catch (Exception e) {
+			entity = null;
+			DBLog.debug("解析查询结果集出错", e);
+			throw new IllegalArgumentException(e);
+		} finally {
+			closeCursor(cursor);
+		}
+		return entity;
+	}
+
+	/**
+	 * 查询实体类总数解析
+	 * @param cursor
+	 * @return
+	 */
+	public static long parseCursorTotal(Cursor cursor) {
+		String value = parseCursorFirstCol(cursor);
+		if(value == null) {
+			value = "0";
+		}
+		return Long.parseLong(value);
+	}
+	
+	/**
+	 * 解析游标结果的第一条记录的第一列的字段值
+	 * @param cursor
+	 * @return
+	 */
+	public static String parseCursorFirstCol(Cursor cursor) {
+		String value = null;
+		if(!isCursorRight(cursor)) {
+			return value;
+		}
+		try {
+			cursor.moveToFirst();
+			value = cursor.getString(0);
+		} catch (Exception e) {
+			value = null;
+			DBLog.debug("解析实体类第一列结果出错", e);
+		} finally {
+			closeCursor(cursor);
+		}
+		return value;
+	}
+}

+ 81 - 0
VideoSqlHelper/src/main/java/com/yc/database/utils/DBLog.java

@@ -0,0 +1,81 @@
+package com.yc.database.utils;
+
+import android.util.Log;
+
+/**
+ * <pre>
+ *     @author yangchong
+ *     email  : yangchong211@163.com
+ *     time  : 2017/8/6
+ *     desc  : 日志工具类
+ *     revise:
+ * </pre>
+ */
+public class DBLog {
+	/**
+	 * 日志标签
+	 */
+	private static final String TAG = "YCSqlLog";
+	/**
+	 * 是否调试模式
+	 */
+	private static boolean IS_DEBUG = true;
+	
+	public static boolean isDebug() {
+		return IS_DEBUG;
+	}
+	
+	/**
+	 * 是否开启调试模式
+	 * @param enable
+	 */
+	public static final void debugEnable(boolean enable) {
+		IS_DEBUG = enable;
+	}
+	
+	public static final void debug(Object msg) {
+		debug(msg.toString());
+	}
+	
+	public static final void debug(String msg) {
+		if(IS_DEBUG) {
+			Log.i(TAG, msg);
+		}
+	}
+	
+	public static final void debug(String msg, Throwable e) {
+		if(IS_DEBUG) {
+			Log.i(TAG, msg, e);
+		}
+	}
+	
+	public static final void debugSql(String sql, Object[] params) {
+		if(IS_DEBUG) {
+			StringBuilder sb = new StringBuilder();
+			for(int i = 0; i < params.length; i++) {
+				if(sb.length() <= 0) {
+					sb.append(",");
+				}
+				sb.append(params.toString());
+			}
+			Log.i(TAG, "{SQL:" + sql + ",PARAMS:" + sb.toString() + "}");
+		}
+	}
+	
+	public static final void debugSql(String sql, Object[] params, Throwable e) {
+		if(IS_DEBUG) {
+			StringBuilder sb = new StringBuilder();
+			for(int i = 0; i < params.length; i++) {
+				if(sb.length() <= 0) {
+					sb.append(",");
+				}
+				sb.append(params.toString());
+			}
+			Log.i(TAG, "{SQL:" + sql + ",PARAMS:" + sb.toString() + "}", e);
+		}
+	}
+	
+	public static final void debug(String format, Object...objects) {
+		debug(String.format(format, objects));
+	}
+}

+ 407 - 0
VideoSqlHelper/src/main/java/com/yc/database/utils/DateUtil.java

@@ -0,0 +1,407 @@
+package com.yc.database.utils;
+
+import android.annotation.SuppressLint;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.Locale;
+
+/**
+ * <pre>
+ *     @author yangchong
+ *     email  : yangchong211@163.com
+ *     time  : 2017/8/6
+ *     desc  : 时间工具类
+ *     revise:
+ * </pre>
+ */
+@SuppressLint("SimpleDateFormat")
+public class DateUtil {
+
+	private static final SimpleDateFormat datetimeFormat = new SimpleDateFormat(
+            "yyyy-MM-dd HH:mm:ss");  
+    private static final SimpleDateFormat dateFormat = new SimpleDateFormat(
+            "yyyy-MM-dd");  
+    private static final SimpleDateFormat timeFormat = new SimpleDateFormat(
+            "HH:mm:ss");  
+  
+    /** 
+     * 获得当前日期时间 
+     * <p> 
+     * 日期时间格式yyyy-MM-dd HH:mm:ss 
+     *  
+     * @return 
+     */  
+    public static String currentDatetime() {
+        return datetimeFormat.format(now());  
+    }  
+  
+    /** 
+     * 格式化日期时间 
+     * <p> 
+     * 日期时间格式yyyy-MM-dd HH:mm:ss 
+     *  
+     * @return 
+     */  
+    public static String formatDatetime(Date date) {
+        return datetimeFormat.format(date);  
+    }  
+  
+    /** 
+     * 格式化日期时间 
+     *  
+     * @param date 
+     * @param pattern 
+     *            格式化模式,详见{@link SimpleDateFormat}构造器
+     *            <code>SimpleDateFormat(String pattern)</code> 
+     * @return 
+     */  
+    public static String formatDatetime(Date date, String pattern) {
+        SimpleDateFormat customFormat = (SimpleDateFormat) datetimeFormat
+                .clone();  
+        customFormat.applyPattern(pattern);  
+        return customFormat.format(date);  
+    }  
+  
+    /** 
+     * 获得当前日期 
+     * <p> 
+     * 日期格式yyyy-MM-dd 
+     *  
+     * @return 
+     */  
+    public static String currentDate() {
+        return dateFormat.format(now());  
+    }  
+  
+    /** 
+     * 格式化日期 
+     * <p> 
+     * 日期格式yyyy-MM-dd 
+     *  
+     * @return 
+     */  
+    public static String formatDate(Date date) {
+        return dateFormat.format(date);  
+    }  
+  
+    /** 
+     * 获得当前时间 
+     * <p> 
+     * 时间格式HH:mm:ss 
+     *  
+     * @return 
+     */  
+    public static String currentTime() {
+        return timeFormat.format(now());  
+    }  
+  
+    /** 
+     * 格式化时间 
+     * <p> 
+     * 时间格式HH:mm:ss 
+     *  
+     * @return 
+     */  
+    public static String formatTime(Date date) {
+        return timeFormat.format(date);  
+    }  
+  
+    /** 
+     * 获得当前时间的<code>java.util.Date</code>对象 
+     *  
+     * @return 
+     */  
+    public static Date now() {
+        return new Date();
+    }  
+  
+    public static Calendar calendar() {
+        Calendar cal = GregorianCalendar.getInstance(Locale.CHINESE);
+        cal.setFirstDayOfWeek(Calendar.MONDAY);
+        return cal;  
+    }  
+  
+    /** 
+     * 获得当前时间的毫秒数 
+     * <p> 
+     * 详见{@link System#currentTimeMillis()}
+     *  
+     * @return 
+     */  
+    public static long millis() {  
+        return System.currentTimeMillis();
+    }  
+  
+    
+    
+    /** 
+     *  
+     * 获得当前Chinese月份 
+     *  
+     * @return 
+     */  
+    public static int month() {  
+        return calendar().get(Calendar.MONTH) + 1;
+    }  
+    
+    /** 
+     *  
+     * 获得当前Chinese年份 
+     *  
+     * @return 
+     */ 
+    public static int year() {
+    	return calendar().get(Calendar.YEAR);
+    }
+  
+    /** 
+     * 获得月份中的第几天 
+     *  
+     * @return 
+     */  
+    public static int dayOfMonth() {  
+        return calendar().get(Calendar.DAY_OF_MONTH);
+    }  
+  
+    /** 
+     * 今天是星期的第几天 
+     *  
+     * @return 
+     */  
+    public static int dayOfWeek() {  
+        return calendar().get(Calendar.DAY_OF_WEEK);
+    }  
+  
+    /** 
+     * 今天是年中的第几天 
+     *  
+     * @return 
+     */  
+    public static int dayOfYear() {  
+        return calendar().get(Calendar.DAY_OF_YEAR);
+    }  
+  
+    /** 
+     *判断原日期是否在目标日期之前 
+     *  
+     * @param src 
+     * @param dst 
+     * @return 
+     */  
+    public static boolean isBefore(Date src, Date dst) {
+        return src.before(dst);  
+    }  
+  
+    /** 
+     *判断原日期是否在目标日期之后 
+     *  
+     * @param src 
+     * @param dst 
+     * @return 
+     */  
+    public static boolean isAfter(Date src, Date dst) {
+        return src.after(dst);  
+    }  
+  
+    /** 
+     *判断两日期是否相同 
+     *  
+     * @param date1 
+     * @param date2 
+     * @return 
+     */  
+    public static boolean isEqual(Date date1, Date date2) {
+        return date1.compareTo(date2) == 0;  
+    }  
+  
+    /** 
+     * 判断某个日期是否在某个日期范围 
+     *  
+     * @param beginDate 
+     *            日期范围开始 
+     * @param endDate 
+     *            日期范围结束 
+     * @param src 
+     *            需要判断的日期 
+     * @return 
+     */  
+    public static boolean between(Date beginDate, Date endDate, Date src) {
+        return beginDate.before(src) && endDate.after(src);  
+    }  
+  
+    /** 
+     * 获得当前月的最后一天 
+     * <p> 
+     * HH:mm:ss为0,毫秒为999 
+     *  
+     * @return 
+     */  
+    public static Date lastDayOfMonth() {
+        Calendar cal = calendar();
+        cal.set(Calendar.DAY_OF_MONTH, 0); // M月置零
+        cal.set(Calendar.HOUR_OF_DAY, 0);// H置零
+        cal.set(Calendar.MINUTE, 0);// m置零
+        cal.set(Calendar.SECOND, 0);// s置零
+        cal.set(Calendar.MILLISECOND, 0);// S置零
+        cal.set(Calendar.MONTH, cal.get(Calendar.MONTH) + 1);// 月份+1
+        cal.set(Calendar.MILLISECOND, -1);// 毫秒-1
+        return cal.getTime();  
+    }  
+  
+    /** 
+     * 获得当前月的第一天 
+     * <p> 
+     * HH:mm:ss SS为零 
+     *  
+     * @return 
+     */  
+    public static Date firstDayOfMonth() {
+        Calendar cal = calendar();
+        cal.set(Calendar.DAY_OF_MONTH, 1); // M月置1
+        cal.set(Calendar.HOUR_OF_DAY, 0);// H置零
+        cal.set(Calendar.MINUTE, 0);// m置零
+        cal.set(Calendar.SECOND, 0);// s置零
+        cal.set(Calendar.MILLISECOND, 0);// S置零
+        return cal.getTime();  
+    }  
+  
+    private static Date weekDay(int week) {
+        Calendar cal = calendar();
+        cal.set(Calendar.DAY_OF_WEEK, week);
+        return cal.getTime();  
+    }  
+  
+    /** 
+     * 获得周五日期 
+     * <p> 
+     * 注:日历工厂方法{@link #calendar()}设置类每个星期的第一天为Monday,US等每星期第一天为sunday 
+     *  
+     * @return 
+     */  
+    public static Date friday() {
+        return weekDay(Calendar.FRIDAY);
+    }  
+  
+    /** 
+     * 获得周六日期 
+     * <p> 
+     * 注:日历工厂方法{@link #calendar()}设置类每个星期的第一天为Monday,US等每星期第一天为sunday 
+     *  
+     * @return 
+     */  
+    public static Date saturday() {
+        return weekDay(Calendar.SATURDAY);
+    }  
+  
+    /** 
+     * 获得周日日期 
+     * <p> 
+     * 注:日历工厂方法{@link #calendar()}设置类每个星期的第一天为Monday,US等每星期第一天为sunday 
+     *  
+     * @return 
+     */  
+    public static Date sunday() {
+        return weekDay(Calendar.SUNDAY);
+    }  
+  
+    /** 
+     * 将字符串日期时间转换成java.util.Date类型 
+     * <p> 
+     * 日期时间格式yyyy-MM-dd HH:mm:ss 
+     *  
+     * @param datetime 
+     * @return 
+     */  
+    public static Date parseDatetime(String datetime) throws ParseException {
+        return datetimeFormat.parse(datetime);  
+    }  
+  
+    /** 
+     * 将字符串日期转换成java.util.Date类型 
+     *<p> 
+     * 日期时间格式yyyy-MM-dd 
+     *  
+     * @param date 
+     * @return 
+     * @throws ParseException
+     */  
+    public static Date parseDate(String date) throws ParseException {
+        return dateFormat.parse(date);  
+    }  
+  
+    /** 
+     * 将字符串日期转换成java.util.Date类型 
+     *<p> 
+     * 时间格式 HH:mm:ss 
+     *  
+     * @param time 
+     * @return 
+     * @throws ParseException
+     */  
+    public static Date parseTime(String time) throws ParseException {
+        return timeFormat.parse(time);  
+    }  
+  
+    /** 
+     * 根据自定义pattern将字符串日期转换成java.util.Date类型 
+     *  
+     * @param datetime 
+     * @param pattern 
+     * @return 
+     * @throws ParseException
+     */  
+    public static Date parseDatetime(String datetime, String pattern)
+            throws ParseException {
+        SimpleDateFormat format = (SimpleDateFormat) datetimeFormat.clone();
+        format.applyPattern(pattern);  
+        return format.parse(datetime);  
+    }
+    
+    /** 
+	 * 得到几天前的时间 
+	 * @param d 
+	 * @param day 
+	 * @return 
+	 */  
+	public static Date getDateBefore(Date d, int day){
+		 Calendar now = Calendar.getInstance();
+		 now.setTime(d);  
+		 now.set(Calendar.DATE, now.get(Calendar.DATE) - day);
+		 return now.getTime();  
+	}
+	
+	/**
+	 * 描述:得到几天前的时间 
+	 * @author:huyongli
+	 * @time:2014-7-31下午05:04:43
+	 * @param date		时间字符串
+	 * @param pattern	该时间字符串的格式
+	 * @param day		天数
+	 * @return
+	 * @throws ParseException
+	 */
+	public static Date getDateBefore(String date, String pattern, int day) throws ParseException {
+		Date d = DateUtil.parseDatetime(date, pattern);
+		Calendar now = Calendar.getInstance();
+		now.setTime(d);  
+		now.set(Calendar.DATE, now.get(Calendar.DATE) - day);
+		return now.getTime();  
+	}
+	
+	/** 
+	   * 得到几天后的时间 
+	   * @param d 
+	   * @param day 
+	   * @return 
+	   */  
+	  public static Date getDateAfter(Date d, int day){
+		   Calendar now = Calendar.getInstance();
+		   now.setTime(d);  
+		   now.set(Calendar.DATE, now.get(Calendar.DATE) + day);
+		   return now.getTime();  
+	  }
+}

+ 119 - 0
VideoSqlHelper/src/main/java/com/yc/database/utils/FieldUtil.java

@@ -0,0 +1,119 @@
+package com.yc.database.utils;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+
+import com.yc.database.manager.FieldTypeManager;
+/**
+ * <pre>
+ *     @author yangchong
+ *     email  : yangchong211@163.com
+ *     time  : 2017/8/6
+ *     desc  : Field工具类
+ *     revise:
+ * </pre>
+ */
+public class FieldUtil {
+	
+	/**
+	 * 获取任意类型字段的get方法
+	 * Author: hyl
+	 * Time: 2015-8-16下午10:12:48
+	 * @param mClass
+	 * @param field
+	 * @return
+	 */
+	public static Method getFieldGetMethod(Class<?> mClass, Field field) {
+		if(FieldTypeManager.getFieldType(field) == FieldTypeManager.BASE_TYPE_BOOLEAN) {//boolean类型
+			return getBooleanFieldGetMethod(mClass, field);
+		}
+		try {
+			String methodName = "get" + field.getName().substring(0, 1).toUpperCase() + field.getName().substring(1);
+			return mClass.getDeclaredMethod(methodName);
+		} catch (NoSuchMethodException e) {
+			throw new NullPointerException("没有为字段[" + field.getName() + "]按照Java代码规范定义get方法,同时请严格按照驼峰命名法进行变量命名");
+		}
+	}
+	
+	/**
+	 * 获取Boolean类型字段的get方法
+	 * Author: hyl
+	 * Time: 2015-8-16下午10:13:01
+	 * @param mClass
+	 * @param field
+	 * @return
+	 */
+	public static Method getBooleanFieldGetMethod(Class<?> mClass, Field field) {
+		try {
+			String methodName = "";
+			if(isFieldStartWithIs(field.getName())) {
+				methodName = field.getName();
+			} else {
+				methodName = "is" + field.getName().substring(0, 1).toUpperCase() + field.getName().substring(1);
+			}
+			return mClass.getDeclaredMethod(methodName);
+		} catch (NoSuchMethodException e) {
+			throw new NullPointerException("没有为字段[" + field.getName() + "]按照Java代码规范定义get方法,同时请严格按照驼峰命名法进行变量命名");
+		}
+	}
+	
+	/**
+	 * 获取任意类型字段的set方法
+	 * Author: hyl
+	 * Time: 2015-8-16下午10:13:32
+	 * @param mClass
+	 * @param field
+	 * @return
+	 */
+	public static Method getFieldSetMethod(Class<?> mClass, Field field) {
+		if(FieldTypeManager.getFieldType(field) == FieldTypeManager.BASE_TYPE_BOOLEAN) {//boolean类型
+			return getBooleanFieldSetMethod(mClass, field);
+		}
+		try {
+			String methodName = "set" + field.getName().substring(0, 1).toUpperCase() + field.getName().substring(1);
+			return mClass.getDeclaredMethod(methodName, field.getType());
+		} catch (NoSuchMethodException e) {
+			throw new NullPointerException("没有为字段[" + field.getName() + "]按照Java代码规范定义set方法,同时请严格按照驼峰命名法进行变量命名");
+		}
+	}
+	
+	/**
+	 * 获取Boolean类型字段的set方法
+	 * Author: hyl
+	 * Time: 2015-8-16下午10:13:43
+	 * @param mClass
+	 * @param field
+	 * @return
+	 */
+	public static Method getBooleanFieldSetMethod(Class<?> mClass, Field field) {
+		try {
+			String methodName = "";
+			if(isFieldStartWithIs(field.getName())) {
+				methodName = "set" + field.getName().substring(2, 3).toUpperCase() + field.getName().substring(3);
+			} else {
+				methodName = "set" + field.getName().substring(0, 1).toUpperCase() + field.getName().substring(1);
+			}
+			return mClass.getDeclaredMethod(methodName, field.getType());
+		} catch (NoSuchMethodException e) {
+			throw new NullPointerException("没有为字段[" + field.getName() + "]按照Java代码规范定义set方法,同时请严格按照驼峰命名法进行变量命名");
+		}
+	}
+
+	/**
+	 * 判断某个字段是否为is开头
+	 * Author: hyl
+	 * Time: 2015-8-16下午10:13:56
+	 * @param fieldName
+	 * @return
+	 */
+	public static boolean isFieldStartWithIs(String fieldName) {
+		if(ValueUtil.isEmpty(fieldName) || fieldName.length() < 3) {
+			return false;
+		}
+		//必须以 is 开头,并且is之后的第一个字母为大写,比如:isAuto,该字段的get、set分别为:isAuto,setAuto
+		if(fieldName.startsWith("is") && Character.isUpperCase(fieldName.charAt(2))) {
+			return true;
+		}
+		return false;
+	}
+}

+ 18 - 0
VideoSqlHelper/src/main/java/com/yc/database/utils/ValueUtil.java

@@ -0,0 +1,18 @@
+package com.yc.database.utils;
+
+public class ValueUtil {
+
+	public static boolean isEmpty(String value) {
+		if(value == null || value.length() == 0) {
+			return true;
+		}
+		return false;
+	}
+	
+	public static boolean isEmpty(Object value) {
+		if(value == null) {
+			return true;
+		}
+		return isEmpty(value.toString());
+	}
+}

+ 1 - 0
VideoSqlLite/.gitignore

@@ -0,0 +1 @@
+/build

+ 25 - 0
VideoSqlLite/build.gradle

@@ -0,0 +1,25 @@
+apply plugin: 'com.android.library'
+
+android {
+    compileSdkVersion 29
+    buildToolsVersion "29.0.3"
+
+    defaultConfig {
+        minSdkVersion 17
+        targetSdkVersion 29
+        versionCode 1
+        versionName "1.0"
+    }
+
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+        }
+    }
+}
+
+dependencies {
+    implementation fileTree(dir: "libs", include: ["*.jar"])
+    implementation project(':VideoSqlHelper')
+}

+ 0 - 0
VideoSqlLite/consumer-rules.pro


+ 21 - 0
VideoSqlLite/proguard-rules.pro

@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile

+ 5 - 0
VideoSqlLite/src/main/AndroidManifest.xml

@@ -0,0 +1,5 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.yc.videosqllite">
+
+
+</manifest>

+ 78 - 0
VideoSqlLite/src/main/java/com/yc/videosqllite/cache/InterCache.java

@@ -0,0 +1,78 @@
+package com.yc.videosqllite.cache;
+
+import java.util.Set;
+
+
+
+/**
+ * <pre>
+ *     @author yangchong
+ *     email  : yangchong211@163.com
+ *     time  : 2020/8/6
+ *     desc  : 缓存接口
+ *     revise:
+ * </pre>
+ */
+public interface InterCache<K, V> {
+
+    /**
+     * 返回当前缓存已占用的总 size
+     *
+     * @return {@code size}
+     */
+    int size();
+
+    /**
+     * 返回当前缓存所能允许的最大 size
+     *
+     * @return {@code maxSize}
+     */
+    int getMaxSize();
+
+    /**
+     * 返回这个 {@code key} 在缓存中对应的 {@code value}, 如果返回 {@code null} 说明这个 {@code key} 没有对应的 {@code value}
+     *
+     * @param key {@code key}
+     * @return {@code value}
+     */
+    V get(K key);
+
+    /**
+     * 将 {@code key} 和 {@code value} 以条目的形式加入缓存,如果这个 {@code key} 在缓存中已经有对应的 {@code value}
+     * 则此 {@code value} 被新的 {@code value} 替换并返回,如果为 {@code null} 说明是一个新条目
+     *
+     * @param key   {@code key}
+     * @param value {@code value}
+     * @return 如果这个 {@code key} 在容器中已经储存有 {@code value}, 则返回之前的 {@code value} 否则返回 {@code null}
+     */
+    V put(K key, V value);
+
+    /**
+     * 移除缓存中这个 {@code key} 所对应的条目,并返回所移除条目的 value
+     * 如果返回为 {@code null} 则有可能时因为这个 {@code key} 对应的 value 为 {@code null} 或条目不存在
+     *
+     * @param key {@code key}
+     * @return 如果这个 {@code key} 在容器中已经储存有 {@code value} 并且删除成功则返回删除的 {@code value}, 否则返回 {@code null}
+     */
+    V remove(K key);
+
+    /**
+     * 如果这个 {@code key} 在缓存中有对应的 value 并且不为 {@code null}, 则返回 {@code true}
+     *
+     * @param key {@code key}
+     * @return {@code true} 为在容器中含有这个 {@code key}, 否则为 {@code false}
+     */
+    boolean containsKey(K key);
+
+    /**
+     * 返回当前缓存中含有的所有 {@code key}
+     *
+     * @return {@code keySet}
+     */
+    Set<K> keySet();
+
+    /**
+     * 清除缓存中所有的内容
+     */
+    void clear();
+}

+ 342 - 0
VideoSqlLite/src/main/java/com/yc/videosqllite/cache/SystemLruCache.java

@@ -0,0 +1,342 @@
+package com.yc.videosqllite.cache;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+
+/**
+ * <pre>
+ *     @author yangchong
+ *     email  : yangchong211@163.com
+ *     time  : 2020/8/6
+ *     desc  : 系统自带LruCache
+ *     revise:
+ * </pre>
+ */
+public class SystemLruCache<K, V> {
+
+    private final LinkedHashMap<K, V> map;
+
+    /** Size of this cache in units. Not necessarily the number of elements. */
+    private int size;
+    private int maxSize;
+
+    private int putCount;
+    private int createCount;
+    private int evictionCount;
+    private int hitCount;
+    private int missCount;
+
+    /**
+     * @param maxSize for caches that do not override {@link #sizeOf}, this is
+     *     the maximum number of entries in the cache. For all other caches,
+     *     this is the maximum sum of the sizes of the entries in this cache.
+     */
+    public SystemLruCache(int maxSize) {
+        if (maxSize <= 0) {
+            throw new IllegalArgumentException("maxSize <= 0");
+        }
+        this.maxSize = maxSize;
+        this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
+    }
+
+    /**
+     * Sets the size of the cache.
+     * @param maxSize The new maximum size.
+     *
+     * @hide
+     */
+    public void resize(int maxSize) {
+        if (maxSize <= 0) {
+            throw new IllegalArgumentException("maxSize <= 0");
+        }
+
+        synchronized (this) {
+            this.maxSize = maxSize;
+        }
+        trimToSize(maxSize);
+    }
+
+    /**
+     * Returns the value for {@code key} if it exists in the cache or can be
+     * created by {@code #create}. If a value was returned, it is moved to the
+     * head of the queue. This returns null if a value is not cached and cannot
+     * be created.
+     */
+    public final V get(K key) {
+        if (key == null) {
+            throw new NullPointerException("key == null");
+        }
+
+        V mapValue;
+        synchronized (this) {
+            mapValue = map.get(key);
+            if (mapValue != null) {
+                hitCount++;
+                return mapValue;
+            }
+            missCount++;
+        }
+
+        /*
+         * Attempt to create a value. This may take a long time, and the map
+         * may be different when create() returns. If a conflicting value was
+         * added to the map while create() was working, we leave that value in
+         * the map and release the created value.
+         */
+
+        V createdValue = create(key);
+        if (createdValue == null) {
+            return null;
+        }
+
+        synchronized (this) {
+            createCount++;
+            mapValue = map.put(key, createdValue);
+
+            if (mapValue != null) {
+                // There was a conflict so undo that last put
+                map.put(key, mapValue);
+            } else {
+                size += safeSizeOf(key, createdValue);
+            }
+        }
+
+        if (mapValue != null) {
+            entryRemoved(false, key, createdValue, mapValue);
+            return mapValue;
+        } else {
+            trimToSize(maxSize);
+            return createdValue;
+        }
+    }
+
+    /**
+     * Caches {@code value} for {@code key}. The value is moved to the head of
+     * the queue.
+     *
+     * @return the previous value mapped by {@code key}.
+     */
+    public final V put(K key, V value) {
+        if (key == null || value == null) {
+            throw new NullPointerException("key == null || value == null");
+        }
+
+        V previous;
+        synchronized (this) {
+            putCount++;
+            size += safeSizeOf(key, value);
+            previous = map.put(key, value);
+            if (previous != null) {
+                size -= safeSizeOf(key, previous);
+            }
+        }
+
+        if (previous != null) {
+            entryRemoved(false, key, previous, value);
+        }
+
+        trimToSize(maxSize);
+        return previous;
+    }
+
+    /**
+     * @param maxSize the maximum size of the cache before returning. May be -1
+     *     to evict even 0-sized elements.
+     */
+    private void trimToSize(int maxSize) {
+        while (true) {
+            K key;
+            V value;
+            synchronized (this) {
+                if (size < 0 || (map.isEmpty() && size != 0)) {
+                    throw new IllegalStateException(getClass().getName()
+                            + ".sizeOf() is reporting inconsistent results!");
+                }
+
+                if (size <= maxSize) {
+                    break;
+                }
+
+                // BEGIN LAYOUTLIB CHANGE
+                // get the last item in the linked list.
+                // This is not efficient, the goal here is to minimize the changes
+                // compared to the platform version.
+                Map.Entry<K, V> toEvict = null;
+                for (Map.Entry<K, V> entry : map.entrySet()) {
+                    toEvict = entry;
+                }
+                // END LAYOUTLIB CHANGE
+
+                if (toEvict == null) {
+                    break;
+                }
+
+                key = toEvict.getKey();
+                value = toEvict.getValue();
+                map.remove(key);
+                size -= safeSizeOf(key, value);
+                evictionCount++;
+            }
+
+            entryRemoved(true, key, value, null);
+        }
+    }
+
+    /**
+     * Removes the entry for {@code key} if it exists.
+     *
+     * @return the previous value mapped by {@code key}.
+     */
+    public final V remove(K key) {
+        if (key == null) {
+            throw new NullPointerException("key == null");
+        }
+
+        V previous;
+        synchronized (this) {
+            previous = map.remove(key);
+            if (previous != null) {
+                size -= safeSizeOf(key, previous);
+            }
+        }
+
+        if (previous != null) {
+            entryRemoved(false, key, previous, null);
+        }
+
+        return previous;
+    }
+
+    /**
+     * Called for entries that have been evicted or removed. This method is
+     * invoked when a value is evicted to make space, removed by a call to
+     * {@link #remove}, or replaced by a call to {@link #put}. The default
+     * implementation does nothing.
+     *
+     * <p>The method is called without synchronization: other threads may
+     * access the cache while this method is executing.
+     *
+     * @param evicted true if the entry is being removed to make space, false
+     *     if the removal was caused by a {@link #put} or {@link #remove}.
+     * @param newValue the new value for {@code key}, if it exists. If non-null,
+     *     this removal was caused by a {@link #put}. Otherwise it was caused by
+     *     an eviction or a {@link #remove}.
+     */
+    protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {}
+
+    /**
+     * Called after a cache miss to compute a value for the corresponding key.
+     * Returns the computed value or null if no value can be computed. The
+     * default implementation returns null.
+     *
+     * <p>The method is called without synchronization: other threads may
+     * access the cache while this method is executing.
+     *
+     * <p>If a value for {@code key} exists in the cache when this method
+     * returns, the created value will be released with {@link #entryRemoved}
+     * and discarded. This can occur when multiple threads request the same key
+     * at the same time (causing multiple values to be created), or when one
+     * thread calls {@link #put} while another is creating a value for the same
+     * key.
+     */
+    protected V create(K key) {
+        return null;
+    }
+
+    private int safeSizeOf(K key, V value) {
+        int result = sizeOf(key, value);
+        if (result < 0) {
+            throw new IllegalStateException("Negative size: " + key + "=" + value);
+        }
+        return result;
+    }
+
+    /**
+     * Returns the size of the entry for {@code key} and {@code value} in
+     * user-defined units.  The default implementation returns 1 so that size
+     * is the number of entries and max size is the maximum number of entries.
+     *
+     * <p>An entry's size must not change while it is in the cache.
+     */
+    protected int sizeOf(K key, V value) {
+        return 1;
+    }
+
+    /**
+     * Clear the cache, calling {@link #entryRemoved} on each removed entry.
+     */
+    public final void evictAll() {
+        trimToSize(-1); // -1 will evict 0-sized elements
+    }
+
+    /**
+     * For caches that do not override {@link #sizeOf}, this returns the number
+     * of entries in the cache. For all other caches, this returns the sum of
+     * the sizes of the entries in this cache.
+     */
+    public synchronized final int size() {
+        return size;
+    }
+
+    /**
+     * For caches that do not override {@link #sizeOf}, this returns the maximum
+     * number of entries in the cache. For all other caches, this returns the
+     * maximum sum of the sizes of the entries in this cache.
+     */
+    public synchronized final int maxSize() {
+        return maxSize;
+    }
+
+    /**
+     * Returns the number of times {@link #get} returned a value that was
+     * already present in the cache.
+     */
+    public synchronized final int hitCount() {
+        return hitCount;
+    }
+
+    /**
+     * Returns the number of times {@link #get} returned null or required a new
+     * value to be created.
+     */
+    public synchronized final int missCount() {
+        return missCount;
+    }
+
+    /**
+     * Returns the number of times {@link #create(Object)} returned a value.
+     */
+    public synchronized final int createCount() {
+        return createCount;
+    }
+
+    /**
+     * Returns the number of times {@link #put} was called.
+     */
+    public synchronized final int putCount() {
+        return putCount;
+    }
+
+    /**
+     * Returns the number of values that have been evicted.
+     */
+    public synchronized final int evictionCount() {
+        return evictionCount;
+    }
+
+    /**
+     * Returns a copy of the current contents of the cache, ordered from least
+     * recently accessed to most recently accessed.
+     */
+    public synchronized final Map<K, V> snapshot() {
+        return new LinkedHashMap<K, V>(map);
+    }
+
+    @Override public synchronized final String toString() {
+        int accesses = hitCount + missCount;
+        int hitPercent = accesses != 0 ? (100 * hitCount / accesses) : 0;
+        return String.format("LruCache[maxSize=%d,hits=%d,misses=%d,hitRate=%d%%]",
+                maxSize, hitCount, missCount, hitPercent);
+    }
+}

+ 205 - 0
VideoSqlLite/src/main/java/com/yc/videosqllite/cache/VideoLruCache.java

@@ -0,0 +1,205 @@
+package com.yc.videosqllite.cache;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+
+
+
+/**
+ * <pre>
+ *     @author yangchong
+ *     email  : yangchong211@163.com
+ *     time  : 2020/8/6
+ *     desc  : LRU 即最近最少使用
+ *     revise: 当缓存满了, 会优先淘汰那些最近最不常访问的数据
+ *             此种缓存策略为框架默认提供, 可自行实现其他缓存策略, 如磁盘缓存
+ * </pre>
+ */
+public class VideoLruCache<K, V> implements InterCache<K, V> {
+
+    private final LinkedHashMap<K, V> cache = new LinkedHashMap<>(100, 0.75f, true);
+    private final int initialMaxSize;
+    private int maxSize;
+    private int currentSize = 0;
+
+    /**
+     * Constructor for LruCache.
+     *
+     * @param size 这个缓存的最大 size,这个 size 所使用的单位必须和 {@link #getItemSize(Object)} 所使用的单位一致.
+     */
+    public VideoLruCache(int size) {
+        if (size <= 0) {
+            throw new IllegalArgumentException("size <= 0");
+        }
+        this.initialMaxSize = size;
+        this.maxSize = size;
+    }
+
+    /**
+     * 设置一个系数应用于当时构造函数中所传入的 size, 从而得到一个新的 {@link #maxSize}
+     * 并会立即调用 {@link #evict} 开始清除满足条件的条目
+     *
+     * @param multiplier 系数
+     */
+    public synchronized void setSizeMultiplier(float multiplier) {
+        if (multiplier < 0) {
+            throw new IllegalArgumentException("Multiplier must be >= 0");
+        }
+        maxSize = Math.round(initialMaxSize * multiplier);
+        evict();
+    }
+
+    /**
+     * 返回每个 {@code item} 所占用的 size,默认为1,这个 size 的单位必须和构造函数所传入的 size 一致
+     * 子类可以重写这个方法以适应不同的单位,比如说 bytes
+     *
+     * @param item 每个 {@code item} 所占用的 size
+     * @return 单个 item 的 {@code size}
+     */
+    protected int getItemSize(V item) {
+        return 1;
+    }
+
+    /**
+     * 当缓存中有被驱逐的条目时,会回调此方法,默认空实现,子类可以重写这个方法
+     *
+     * @param key   被驱逐条目的 {@code key}
+     * @param value 被驱逐条目的 {@code value}
+     */
+    protected void onItemEvicted(K key, V value) {
+        // optional override
+    }
+
+    /**
+     * 返回当前缓存所能允许的最大 size
+     *
+     * @return {@code maxSize}
+     */
+    @Override
+    public synchronized int getMaxSize() {
+        return maxSize;
+    }
+
+    /**
+     * 返回当前缓存已占用的总 size
+     *
+     * @return {@code size}
+     */
+    @Override
+    public synchronized int size() {
+        return currentSize;
+    }
+
+    /**
+     * 如果这个 {@code key} 在缓存中有对应的 {@code value} 并且不为 {@code null},则返回 true
+     *
+     * @param key 用来映射的 {@code key}
+     * @return {@code true} 为在容器中含有这个 {@code key}, 否则为 {@code false}
+     */
+    @Override
+    public synchronized boolean containsKey(K key) {
+        return cache.containsKey(key);
+    }
+
+    /**
+     * 返回当前缓存中含有的所有 {@code key}
+     *
+     * @return {@code keySet}
+     */
+    @Override
+    public synchronized Set<K> keySet() {
+        return cache.keySet();
+    }
+
+    /**
+     * 返回这个 {@code key} 在缓存中对应的 {@code value}, 如果返回 {@code null} 说明这个 {@code key} 没有对应的 {@code value}
+     *
+     * @param key 用来映射的 {@code key}
+     * @return {@code value}
+     */
+    @Override
+    public synchronized V get(K key) {
+        return cache.get(key);
+    }
+
+    /**
+     * 将 {@code key} 和 {@code value} 以条目的形式加入缓存,如果这个 {@code key} 在缓存中已经有对应的 {@code value}
+     * 则此 {@code value} 被新的 {@code value} 替换并返回,如果为 {@code null} 说明是一个新条目
+     * <p>
+     * 如果 {@link #getItemSize} 返回的 size 大于或等于缓存所能允许的最大 size, 则不能向缓存中添加此条目
+     * 此时会回调 {@link #onItemEvicted(Object, Object)} 通知此方法当前被驱逐的条目
+     *
+     * @param key   通过这个 {@code key} 添加条目
+     * @param value 需要添加的 {@code value}
+     * @return 如果这个 {@code key} 在容器中已经储存有 {@code value}, 则返回之前的 {@code value} 否则返回 {@code null}
+     */
+    @Override
+    public synchronized V put(K key, V value) {
+        final int itemSize = getItemSize(value);
+        if (itemSize >= maxSize) {
+            onItemEvicted(key, value);
+            return null;
+        }
+
+        final V result = cache.put(key, value);
+        if (value != null) {
+            currentSize += getItemSize(value);
+        }
+        if (result != null) {
+            currentSize -= getItemSize(result);
+        }
+        evict();
+
+        return result;
+    }
+
+    /**
+     * 移除缓存中这个 {@code key} 所对应的条目,并返回所移除条目的 {@code value}
+     * 如果返回为 {@code null} 则有可能时因为这个 {@code key} 对应的 {@code value} 为 {@code null} 或条目不存在
+     *
+     * @param key 使用这个 {@code key} 移除对应的条目
+     * @return 如果这个 {@code key} 在容器中已经储存有 {@code value} 并且删除成功则返回删除的 {@code value}, 否则返回 {@code null}
+     */
+    @Override
+    public synchronized V remove(K key) {
+        final V value = cache.remove(key);
+        if (value != null) {
+            currentSize -= getItemSize(value);
+        }
+        return value;
+    }
+
+    /**
+     * 清除缓存中所有的内容
+     */
+    @Override
+    public void clear() {
+        trimToSize(0);
+    }
+
+    /**
+     * 当指定的 size 小于当前缓存已占用的总 size 时,会开始清除缓存中最近最少使用的条目
+     *
+     * @param size {@code size}
+     */
+    protected synchronized void trimToSize(int size) {
+        Map.Entry<K, V> last;
+        while (currentSize > size) {
+            last = cache.entrySet().iterator().next();
+            final V toRemove = last.getValue();
+            currentSize -= getItemSize(toRemove);
+            final K key = last.getKey();
+            cache.remove(key);
+            onItemEvicted(key, toRemove);
+        }
+    }
+
+    /**
+     * 当缓存中已占用的总 size 大于所能允许的最大 size ,会使用  {@link #trimToSize(int)} 开始清除满足条件的条目
+     */
+    private void evict() {
+        trimToSize(maxSize);
+    }
+}
+

+ 121 - 0
VideoSqlLite/src/main/java/com/yc/videosqllite/cache/VideoMapCache.java

@@ -0,0 +1,121 @@
+package com.yc.videosqllite.cache;
+
+import com.yc.videosqllite.manager.CacheConfig;
+import com.yc.videosqllite.model.VideoLocation;
+import com.yc.videosqllite.utils.VideoMd5Utils;
+
+/**
+ * <pre>
+ *     @author yangchong
+ *     email  : yangchong211@163.com
+ *     time  : 2020/8/6
+ *     desc  : 内存缓存
+ *     revise:
+ * </pre>
+ */
+public class VideoMapCache {
+
+    private CacheConfig cacheConfig;
+    /**
+     * 缓存
+     */
+    private InterCache<String, VideoLocation> mCache;
+
+    public VideoMapCache(CacheConfig cacheConfig){
+        this.cacheConfig = cacheConfig;
+        //默认设置存储最大值为1000条
+        mCache =  new VideoLruCache<>(1000);
+    }
+
+    /**
+     * 存数据
+     * @param url                           链接
+     * @param location                      视频数据
+     */
+    public synchronized void put(String url , VideoLocation location){
+        if (url==null || url.length()==0){
+            return;
+        }
+        if (location==null){
+            return;
+        }
+        String key = VideoMd5Utils.encryptMD5ToString(url, cacheConfig.getSalt());
+        location.setUrlMd5(key);
+        mCache.put(key,location);
+    }
+
+    /**
+     * 取数据
+     * @param url                           链接
+     * @return
+     */
+    public synchronized long get(String url){
+        if (url==null || url.length()==0){
+            return 0;
+        }
+        String key = VideoMd5Utils.encryptMD5ToString(url, cacheConfig.getSalt());
+        VideoLocation videoLocation = mCache.get(key);
+        if (videoLocation==null){
+            //没找到
+            return 0;
+        }
+        if (videoLocation.getTotalTime()<=videoLocation.getPosition()){
+            //这一步主要是避免外部开发员瞎存播放进度
+            return 0;
+        }
+        long position = videoLocation.getPosition();
+        if (position<0){
+            position = 0;
+        }
+        return position;
+    }
+
+    /**
+     * 移除数据
+     * @param url                           链接
+     * @return
+     */
+    public synchronized boolean remove(String url){
+        if (url==null || url.length()==0){
+            return false;
+        }
+        String key = VideoMd5Utils.encryptMD5ToString(url, cacheConfig.getSalt());
+        VideoLocation location = mCache.remove(key);
+        if (location==null){
+            return false;
+        } else {
+            //移除成功
+            return true;
+        }
+    }
+
+    /**
+     * 是否包含
+     * @param url                           链接
+     * @return
+     */
+    public synchronized boolean containsKey(String url){
+        if (url==null || url.length()==0){
+            return false;
+        }
+        String key = VideoMd5Utils.encryptMD5ToString(url, cacheConfig.getSalt());
+        boolean containsKey = mCache.containsKey(key);
+        return containsKey;
+    }
+
+
+    /**
+     * 清楚所有数据
+     * @return                              是否清楚完毕
+     */
+    public synchronized boolean clearAll(){
+        mCache.clear();
+        int size = mCache.size();
+        if (size==0){
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+}

+ 12 - 0
VideoSqlLite/src/main/java/com/yc/videosqllite/dao/SqlLiteCache.java

@@ -0,0 +1,12 @@
+package com.yc.videosqllite.dao;
+
+import com.yc.videosqllite.manager.CacheConfig;
+
+public class SqlLiteCache {
+
+    public SqlLiteCache(CacheConfig cacheConfig) {
+
+    }
+
+
+}

+ 72 - 0
VideoSqlLite/src/main/java/com/yc/videosqllite/manager/CacheConfig.java

@@ -0,0 +1,72 @@
+package com.yc.videosqllite.manager;
+
+import com.yc.videosqllite.cache.InterCache;
+import com.yc.videosqllite.model.VideoLocation;
+
+/**
+ * <pre>
+ *     @author yangchong
+ *     email  : yangchong211@163.com
+ *     time  : 2020/8/6
+ *     desc  : 配置类
+ *     revise:
+ * </pre>
+ */
+public class CacheConfig {
+
+    /**
+     * 是否生效
+     */
+    private boolean mIsEffective = false;
+    /**
+     * 内存缓存最大值
+     */
+    private int mCacheMax;
+    /**
+     * 对视频链接加盐字符串
+     * 处理md5加密的盐
+     */
+    private String mSalt = "yc_video";
+    /**
+     * 0,表示内存缓存
+     * 1,表示磁盘缓存
+     * 2,表示内存缓存+磁盘缓存
+     */
+    private int type = 0;
+
+
+    public boolean isEffective() {
+        return mIsEffective;
+    }
+
+    public void setIsEffective(boolean mIsEffective) {
+        this.mIsEffective = mIsEffective;
+    }
+
+    public int getCacheMax() {
+        return mCacheMax;
+    }
+
+    public void setCacheMax(int mCacheMax) {
+        this.mCacheMax = mCacheMax;
+    }
+
+    public String getSalt() {
+        return mSalt;
+    }
+
+    public void setSalt(String salt) {
+        //设置盐处理
+        if (salt!=null && salt.length()>0){
+            this.mSalt = salt;
+        }
+    }
+
+    public int getType() {
+        return type;
+    }
+
+    public void setType(int type) {
+        this.type = type;
+    }
+}

+ 104 - 0
VideoSqlLite/src/main/java/com/yc/videosqllite/manager/LocationManager.java

@@ -0,0 +1,104 @@
+package com.yc.videosqllite.manager;
+
+import com.yc.videosqllite.cache.VideoMapCache;
+import com.yc.videosqllite.dao.SqlLiteCache;
+import com.yc.videosqllite.model.VideoLocation;
+
+/**
+ * <pre>
+ *     @author yangchong
+ *     email  : yangchong211@163.com
+ *     time  : 2020/8/6
+ *     desc  : 音视频播放记录本地缓存
+ *     revise:
+ * </pre>
+ */
+public class LocationManager {
+
+    /**
+     * 终极目标
+     * 1.开发者可以自由切换缓存模式
+     * 2.可以设置内存缓存最大值,设置磁盘缓存的路径
+     * 3.能够有增删改查基础方法
+     * 4.多线程下安全和脏数据避免
+     * 5.代码体积小
+     * 6.一键打印存取表结构日志
+     * 7.如何一键将本地记录数据上传
+     * 8.拓展性和封闭性
+     * 9.性能,插入和获取数据,超1000条数据测试
+     * 10.将sql执行sql语句给简化,避免手写sql语句,因为特别容易出问题。而且存取bean如果比较复杂那很难搞
+     */
+
+    private CacheConfig cacheConfig;
+    /**
+     * 内存缓存
+     */
+    private VideoMapCache videoMapCache;
+    /**
+     * 磁盘缓存
+     */
+    private SqlLiteCache sqlLiteCache;
+
+    private static class ManagerHolder {
+        private static final LocationManager INSTANCE = new LocationManager();
+    }
+
+    public static LocationManager getInstance() {
+        return ManagerHolder.INSTANCE;
+    }
+
+    public void init(CacheConfig cacheConfig){
+        this.cacheConfig = cacheConfig;
+        videoMapCache = new VideoMapCache(cacheConfig);
+        sqlLiteCache = new SqlLiteCache(cacheConfig);
+    }
+
+    /**
+     * 存数据
+     * @param url                           链接
+     * @param location                      视频数据
+     */
+    public synchronized void put(String url , VideoLocation location){
+        videoMapCache.put(url,location);
+    }
+
+    /**
+     * 取数据
+     * @param url                           链接
+     * @return
+     */
+    public synchronized long get(String url){
+        long position = videoMapCache.get(url);
+        return position;
+    }
+
+    /**
+     * 移除数据
+     * @param url                           链接
+     * @return
+     */
+    public synchronized boolean remove(String url){
+        boolean remove = videoMapCache.remove(url);
+        return remove;
+    }
+
+    /**
+     * 是否包含
+     * @param url                           链接
+     * @return
+     */
+    public synchronized boolean containsKey(String url){
+        boolean containsKey = videoMapCache.containsKey(url);
+        return containsKey;
+    }
+
+    /**
+     * 清楚所有数据
+     * @return                              是否清楚完毕
+     */
+    public synchronized boolean clearAll(){
+        boolean clearAll = videoMapCache.clearAll();
+        return clearAll;
+    }
+
+}

+ 90 - 0
VideoSqlLite/src/main/java/com/yc/videosqllite/model/VideoLocation.java

@@ -0,0 +1,90 @@
+package com.yc.videosqllite.model;
+
+
+import java.io.Serializable;
+
+/**
+ * <pre>
+ *     @author yangchong
+ *     email  : yangchong211@163.com
+ *     time  : 2020/8/6
+ *     desc  : 音视频bean
+ *     revise: 必须
+ * </pre>
+ */
+public class VideoLocation implements Serializable {
+
+    /**
+     * 视频链接
+     */
+    private String url;
+    /**
+     * 视频链接md5
+     */
+    private String urlMd5;
+    /**
+     * 视频播放位置
+     */
+    private long position;
+    /**
+     * 视频总时间
+     */
+    private long totalTime;
+
+    public VideoLocation(String url, long position, long totalTime) {
+        this.url = url;
+        this.position = position;
+        this.totalTime = totalTime;
+    }
+
+    /*public VideoLocation(String url, String urlMd5, long position, long totalTime) {
+        this.url = url;
+        this.urlMd5 = urlMd5;
+        this.position = position;
+        this.totalTime = totalTime;
+    }*/
+
+    public String getUrl() {
+        return url;
+    }
+
+    public void setUrl(String url) {
+        this.url = url;
+    }
+
+    public String getUrlMd5() {
+        return urlMd5;
+    }
+
+    public void setUrlMd5(String urlMd5) {
+        this.urlMd5 = urlMd5;
+    }
+
+    public long getPosition() {
+        return position;
+    }
+
+    public void setPosition(long position) {
+        this.position = position;
+    }
+
+    public long getTotalTime() {
+        return totalTime;
+    }
+
+    public void setTotalTime(long totalTime) {
+        this.totalTime = totalTime;
+    }
+
+    @Override
+    public String toString() {
+        return "VideoLocation{" +
+                "url='" + url + '\'' +
+                ", urlMd5='" + urlMd5 + '\'' +
+                ", position=" + position +
+                ", totalTime=" + totalTime +
+                '}';
+    }
+
+
+}

+ 109 - 0
VideoSqlLite/src/main/java/com/yc/videosqllite/utils/VideoMd5Utils.java

@@ -0,0 +1,109 @@
+package com.yc.videosqllite.utils;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * <pre>
+ *     @author yangchong
+ *     email  : yangchong211@163.com
+ *     time  : 2020/6/16
+ *     desc  : md加密+加盐工具类
+ *     revise:
+ * </pre>
+ */
+public final class VideoMd5Utils {
+
+
+    private static final char hexDigits[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
+
+    /**
+     * MD5 加密
+     *
+     * @param data 明文字符串
+     * @return 16 进制密文
+     */
+    public static String encryptMD5ToString(final String data) {
+        return encryptMD5ToString(data.getBytes());
+    }
+
+    /**
+     * MD5 加密
+     *
+     * @param data 明文字符串
+     * @param salt 盐
+     * @return 16 进制加盐密文
+     */
+    public static String encryptMD5ToString(final String data, final String salt) {
+        return bytes2HexString(encryptMD5((data + salt).getBytes()));
+    }
+
+    /**
+     * MD5 加密
+     *
+     * @param data 明文字节数组
+     * @return 16 进制密文
+     */
+    public static String encryptMD5ToString(final byte[] data) {
+        return bytes2HexString(encryptMD5(data));
+    }
+
+    /**
+     * MD5 加密
+     *
+     * @param data 明文字节数组
+     * @param salt 盐字节数组
+     * @return 16 进制加盐密文
+     */
+    public static String encryptMD5ToString(final byte[] data, final byte[] salt) {
+        if (data == null || salt == null) return null;
+        byte[] dataSalt = new byte[data.length + salt.length];
+        System.arraycopy(data, 0, dataSalt, 0, data.length);
+        System.arraycopy(salt, 0, dataSalt, data.length, salt.length);
+        return bytes2HexString(encryptMD5(dataSalt));
+    }
+
+    /**
+     * MD5 加密
+     *
+     * @param data 明文字节数组
+     * @return 密文字节数组
+     */
+    public static byte[] encryptMD5(final byte[] data) {
+        return hashTemplate(data, "MD5");
+    }
+
+
+    /**
+     * hash 加密模板
+     *
+     * @param data      数据
+     * @param algorithm 加密算法
+     * @return 密文字节数组
+     */
+    private static byte[] hashTemplate(final byte[] data, final String algorithm) {
+        if (data == null || data.length <= 0) return null;
+        try {
+            MessageDigest md = MessageDigest.getInstance(algorithm);
+            md.update(data);
+            return md.digest();
+        } catch (NoSuchAlgorithmException e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+
+    private static String bytes2HexString(final byte[] bytes) {
+        if (bytes == null) return null;
+        int len = bytes.length;
+        if (len <= 0) return null;
+        char[] ret = new char[len << 1];
+        for (int i = 0, j = 0; i < len; i++) {
+            ret[j++] = hexDigits[bytes[i] >>> 4 & 0x0f];
+            ret[j++] = hexDigits[bytes[i] & 0x0f];
+        }
+        return new String(ret);
+    }
+
+
+}

BIN
image/直播服务架构图.png


BIN
image/视频直播流程图.png


+ 97 - 0
read/09.视频深度优化处理.md

@@ -0,0 +1,97 @@
+# 视频优化处理
+#### 目录介绍
+
+
+
+
+
+### 01.播放的完整流程
+播放器加载一个网络url,首先要进行网络请求,网络如何优化,涉及到网络优化的方方面面。
+网络拉取回来数据之后,识别一下当前视频的具体封装格式,这个可以正式流式视频,也可以是普通视频,优化的手段有点不同。
+识别到具体的封装格式,按照封装格式的要求,开始解析封装格式,解析其中的音频流、视频流、字幕流等等。
+音频流要解码成音频原始数据,视频流要解码成视频原始数据。
+解码过程中注意音视频同步。
+音频播放,同时视频开始渲染。
+
+
+
+### 02.播放痛点
+- 根据我们平时的开发实践,我们总结出播放过程中常见的几类问题:
+
+播放失败率高
+播放首帧慢
+播放卡顿
+播放器占用CPU、内存过高
+
+- 面对这些问题,我们急切需要知道两方面的数据:
+
+怎么监控这些问题
+怎么解决这类问题
+
+这两个问题是有有递进关系的,“怎么监控这些问题”就是为了更好地“解决这类问题”。
+
+
+
+### 03.监控手段
+1、网络加载监控
+播放视频首要的是网络加载,网络请求是一个复杂的过程,全链路的点太多,将全链路的所有点收集起来,可以在播放器中加上网络的全链路监控:
+
+这样我们对网络的整体加载情况有了全面的把握,发生网络加载问题,也知道是哪个点出现了问题,分析解决问题有了更加全的数据。
+2、播放器全链路监控
+开篇就分析了播放器的完整流程,其实开发者也非常需要当前播放器的运行状态:
+播放器的工作状态也可以拆解一下:
+
+播放器发生状态异常,开发者可以明确获知播放器当前所处的状态。
+每个状态都可能发生异常,发生异常都有具体的原因。利用播放器状态、播放器出错情况构建一个较为完善的播放监控体系。
+3、播放器流畅度监控
+播放卡顿,就是播放过程中发生loading,UI直接显示转圈,这对用户体验的损害是巨大的,用户在不断的吐槽中默默地卸载了我们的app。卡顿的主要原因是网络状况不好,很小的一部分原因是源的问题。
+
+卡顿的次数
+卡顿的时长
+卡顿时的网速
+
+单次播放平均卡顿次数和卡顿时间是我们衡量播放流畅度的重要指标。
+如果是源的问题,例如出现播放视频的时候,进度条在走,但是画面不走,就是视频解码出现问题,但是又没有出错,只是解码出的数据有问题。
+解码出的数据有问题,有两种情况:原始数据就存在问题,这种情况下基本无法优化;另一种情况下是解码线程异常。
+
+MediaCodec发生异常
+解码线程异常错误
+
+监控发生问题时系统codec的具体状态,然后上报,便于分析问题。
+
+
+
+### 04.播放成功率优化
+播放失败的原因很多,使用播放器播放视频,最终都会在Player.onError回调中通知开发者播放失败了,最多返回一个错误码,对应一个播放错误。
+总结而言,播放错误主要分为下面几类:
+
+**网络加载错误:**网络请求发生问题,可能是网络请求的任何一个阶段。
+**视频格式识别错误:**不支持当前的格式,或者当前格式识别出错。
+**解码出错:**不支持当前视频、音频解码导致的出错,或者系统codec异常导致的问题。
+**文件的IO异常:**读取缓存文件发生问题。
+
+网络加载错误一般要视情况而定,网络超时要做好超时重试机制。
+视频格式支持使用ffmpeg能解决基本上所有的视频格式的识别和处理工作。
+MediaCodec解码受到手机硬件的制约,解码有时候会出错,出错可以切换到软解码。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ 0 - 0
read/09.视频播放器使用设计模式.md → read/30.视频播放器使用设计模式.md


+ 60 - 0
read/51.直播基础知识点介绍.md

@@ -0,0 +1,60 @@
+# 直播基础知识点
+#### 目录介绍
+- 01.直播简单的介绍
+- 02.发送端介绍
+- 03.接收端介绍
+- 04.常见直播协议
+
+
+
+
+
+
+
+### 01.直播简单的介绍
+- 相信大家或多或少都接触过网络直播,对直播业务都有一定了解:主播通过手机或PC开播,观众通过终端设备进入房间观看直播。
+- 直播业务由三大要素组成:主播、服务器、观众,对应的是发送端、服务器、接收端,其中发送端的行为是自底向上的,接收端的行为是自顶向下的。
+
+
+
+
+### 02.发送端介绍
+- 发送端介绍
+    - 1.主播通过设备的麦克风采集原始音频数据(pcm格式),摄像头采集原始视频数据(yuv格式)
+    - 2.通过编解码工具(如MediaCodec-硬编,ffmpeg-软编)将原始音频、视频数据分别转换成aac和h264格式
+    - 3.通过混合器提取音视频数据中的轨道并封装成flv格式
+    - 4.将flv数据包裹上rtmp协议头并将数据发送到服务器
+
+
+
+### 03.接收端介绍
+- 接收端介绍
+    - 1.观众通过客户端设备进入房间,播放器通过rtmp协议向服务器拉取视频数据流
+    - 2.用rtmp协议解析数据流,得到flv格式的数据流
+    - 3.播放器将flv格式数据解析成音视频数据流(aac,h264)
+    - 4.通过编解码工具将aac和h264解码成原始的音视频数据
+    - 5.调用设备的扬声器播放音频数据,显卡渲染视频数据并在屏幕上显示
+
+
+### 04.常见直播协议
+- 直播协议常见的有三种:RTMP、Http-FLV和HLS。
+    - RTMP: 基于TCP协议,由Adobe设计,将音视频数据切割成小的数据包在互联网上传输,延时3s以内,但拆包组包复杂,在海量并发情况下不稳定。由于不是基于Http协议,存在被防火墙墙掉的可能性。
+    - Http-FLV:基于Http协议,由Adobe设计,在大块音视频数据头部添加标记信息,延时3s以内,海量并发稳定,手机浏览器支持不足。
+    - HLS:基于Http协议,由Apple设计,将视频数据切分成片段(10s以内),由m3u8索引文件进行管理,高延时(10s到30s),手机浏览器支持较好,可通过网页转发直播链接。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ 21 - 0
read/52.直播推流端分析.md

@@ -0,0 +1,21 @@
+# 音频播放器通用框架
+#### 目录介绍
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ 21 - 0
read/53.直播播放端分析.md

@@ -0,0 +1,21 @@
+# 音频播放器通用框架
+#### 目录介绍
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ 3 - 0
settings.gradle

@@ -1,3 +1,6 @@
+include ':VideoSqlHelper'
+include ':VideoSqlLite'
+include ':VideoM3u8'
 include ':MusicPlayer'
 include ':VideoView'
 include ':VideoKernel'