Android解析SRT字幕文件
- 说明一下
这是本人第一次写Markdown,很多写的不规范的还希望大家能够理解,好的废话不多说。
SRT的数据格式
1313
01:45:15,600 –> 01:45:17,240
向「活死人墓」资助人提供服务
and offered his services to the backers of The Tomb.
以上就是SRT的数据格式,通过以上单个数据节点可以提供一个大致的思路是:先定位一个数据节点的固定格式,然后将一行一行的读取到数据。本文使用时间作为固定格式,将通过正则表达式”\d\d:\d\d:\d\d,\d\d\d –> \d\d:\d\d:\d\d,\d\d\d”得到01:45:15,600 –> 01:45:17,240格式的时间。逐行读取保存即可得到该节点的全部数据。
获取数据节点的代码
节点定义
package com.pplive.androidphone.layout.subtitle;
/**
* @Auther jixiongxu
* @date 2017/9/20.
* @descraption 字幕数据结构
*/
public class SubtitlesModel
{
/**
* 当前节点
*/
public int node;
/**
* 开始显示的时间
*/
public int star;
/**
* 结束显示的时间
*/
public int end;
/**
* 显示的内容《英文》
*/
public String contextE;
/**
* 显示的内容《中文》
*/
public String contextC;
}
读取文件和筛选内容
package com.pplive.androidphone.layout.subtitle;
import android.util.Log;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.regex.Pattern;
/**
* @Auther jixiongxu
* @date 2017/9/20.
* @descraption 用于解析字幕
*/
public class SubtitlesDataCoding
{
/**
* 一秒=1000毫秒
*/
private final static int oneSecond = 1000;
private final static int oneMinute = 60 * oneSecond;
private final static int oneHour = 60 * oneMinute;
/**
* 每一个数据节点
*/
public static ArrayList<SubtitlesModel> list = new ArrayList<SubtitlesModel>();
/**
* 正则表达式,判断是否是时间的格式
*/
private final static String equalStringExpress = "\\d\\d:\\d\\d:\\d\\d,\\d\\d\\d --> \\d\\d:\\d\\d:\\d\\d,\\d\\d\\d";
/**
* 读取本地文件
*
* @param path
*/
public static ArrayList<SubtitlesModel> readFile(String path)
{
String line;
FileInputStream is;
File subtitlesFile = new File(path);
BufferedReader in = null;
if (!subtitlesFile.exists() || !subtitlesFile.isFile())
{
Log.e("jixiongxu:", "open subtitle file fill");
return null;
}
/**
* 读取文件,转流,方便读取行
*/
try
{
is = new FileInputStream(subtitlesFile);
try
{
in = new BufferedReader(new InputStreamReader(is, "UTF-8"));
}
catch (UnsupportedEncodingException e)
{
e.printStackTrace();
}
}
catch (FileNotFoundException e)
{
e.printStackTrace();
}
try
{
assert in != null;
while ((line = in.readLine()) != null)
{
SubtitlesModel sm = new SubtitlesModel();
// 匹配正则表达式,不符合提前结束当前行;
if (Pattern.matches(equalStringExpress, line))
{
// 填充开始时间数据
sm.star = getTime(line.substring(0, 12));
// 填充结束时间数据
sm.end = getTime(line.substring(17, 29));
// 填充中文数据
sm.contextC = in.readLine();
// 填充英文数据
sm.contextE = in.readLine();
// 当前字幕的节点位置
sm.node = list.size() + 1;
list.add(sm);
}
}
return getSubtitles();
}
catch (IOException e)
{
e.printStackTrace();
}
if (list != null)
{
Log.d("jixiongxu:", "open subtitle file ok");
return getSubtitles();
}
return null;
}
/**
* @param line
* @return 字幕所在的时间节点
* @descraption 将String类型的时间转换成int的时间类型
*/
private static int getTime(String line)
{
try
{
return Integer.parseInt(line.substring(0, 2)) * oneHour// 时
+ Integer.parseInt(line.substring(3, 5)) * oneMinute// 分
+ Integer.parseInt(line.substring(6, 8)) * oneSecond// 秒
+ Integer.parseInt(line.substring(9, line.length()));// 毫秒
}
catch (NumberFormatException e)
{
e.printStackTrace();
}
return -1;
}
/**
* @return list?null
* @descraption 返回解析后的字幕数据
*/
public static ArrayList<SubtitlesModel> getSubtitles()
{
return list != null && list.size() > 0 ? list : null;
}
}
本文的字幕数据是直接从本地读取,大家在使用的时候如果是从网络单独下载的str或者从视频文件中提琴的文件需要更改这个类的一些方法。因为srt中读取的时间格式是String格式,在读取过程中本文直接转换了成毫秒,这样在调用的时候就不必再转换(因为视频一般是以毫秒为单位播放)。
显示的实现
由上文的返回数据可以看大返回了一个ArrayList< SubtitlesModel >数据,数据节点需要在相应的时间显示出相应的字幕,则需要一个查找过程,为了提高效率本文使用二分查找法进行查找。同时为了提高代码的可扩展性,定义一个接口方便以后再修改。
定义控制接口
package com.pplive.androidphone.layout.subtitle;
import java.util.ArrayList;
/**
* @Auther jixiongxu
* @dat 2017/9/21
* @descraption 字幕控制接口
*/
public interface ISubtitleControl
{
/**
* 设置中文字幕
*
* @param item
*/
void setItemSubtitleChina(String item);
/**
* 设置英文字幕
*
* @param item
*/
void setItemSubtitleEnglish(String item);
/**
* 定位设置字幕
*
* @param position
*/
void seekTo(int position);
/**
* 设置数据
*
* @param list
*/
void setData(ArrayList<SubtitlesModel> list);
/**
* 设置显示的语言
*
* @param type
*/
void setLanguage(int type);
/**
* 暂停
*
* @param pause
*/
void setPause(boolean pause);
/**
* 开始
*
* @param start
*/
void setStart(boolean start);
/**
* 停止
*
* @param stop
*/
void setStop(boolean stop);
/**
* 后台播放
*
* @param pb
*/
void setPlayOnBackground(boolean pb);
}
接下来是布局的编写,显示的字幕中包含两个textview,一个显示中文一个显示英文。由于本文在设计过程中需要对textview的显示效果和点击都有要求,因此还自定义了一个继承与TextView的SubtitleTextView。代码也是非常简单。
SubtitleTextView代码
package com.pplive.androidphone.layout.subtitle;
import android.content.Context;
import android.graphics.Color;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.TextView;
/**
* @Auther jixiongxu
* @date 2017/9/20
* @descraption 显示字幕的字体样式
*/
public class SubtitleTextView extends TextView implements View.OnTouchListener
{
private Context context;
private SubtitleClickListener listener;
public SubtitleTextView(Context context)
{
this(context, null);
}
public SubtitleTextView(Context context, AttributeSet attrs)
{
super(context, attrs);
this.context = context;
// 默认白色字体
setTextColor(Color.WHITE);
setSingleLine(true);
setShadowLayer(3, 0, 0, Color.RED);
this.setOnTouchListener(this);
}
public void setSubtitleOnTouchListener(SubtitleClickListener listener)
{
this.listener = listener;
}
@Override
public boolean onTouch(View view, MotionEvent event)
{
switch (event.getAction())
{
case MotionEvent.ACTION_DOWN:
if (listener != null)
listener.ClickDown();
break;
case MotionEvent.ACTION_UP:
if (listener != null)
listener.ClickUp();
break;
}
return true;
}
}
/**
* 对字幕进行监听的接口
*/
interface SubtitleClickListener
{
/**
* 按下
*/
void ClickDown();
/**
* 取消
*/
void ClickUp();
}
接下来是字幕布局的代码
SubtitleView代码
package com.pplive.androidphone.layout.subtitle;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.Toast;
import com.pplive.androidphone.R;
import com.pplive.androidphone.ui.videoplayer.layout.VideoJjController;
import java.util.ArrayList;
import java.util.zip.Inflater;
/**
* @date 2017/9/20
* @Auther jixiongxu
* @descraptio 显示字幕的图层
*/
public class SubtitleView extends LinearLayout implements ISubtitleControl, SubtitleClickListener
{
/**
* 只显示中文
*/
public final static int LANGUAGE_TYPE_CHINA = 0;
/**
* 只显示英文
*/
public final static int LANGUAGE_TYPE_ENGLISH = LANGUAGE_TYPE_CHINA + 1;
/**
* 双语显示
*/
public final static int LANGUAGE_TYPE_BOTH = LANGUAGE_TYPE_ENGLISH + 1;
/**
* 字幕所有的数据
*/
private ArrayList<SubtitlesModel> data = new ArrayList<SubtitlesModel>();
/**
* 中文字幕
*/
private SubtitleTextView subChina;
/**
* 英文字幕
*/
private SubtitleTextView subEnglish;
/**
* 当前显示节点
*/
private View subTitleView;
/**
* 单条字幕数据
*/
private SubtitlesModel model = null;
/**
* 后台播放
*/
private boolean palyOnBackground = false;
private int language = LANGUAGE_TYPE_BOTH;
private Context context;
public SubtitleView(Context context)
{
this(context, null);
}
public SubtitleView(Context context, AttributeSet attrs, int defStyleAttr)
{
this(context, attrs);
}
public SubtitleView(final Context context, AttributeSet attrs)
{
super(context, attrs);
this.context = context;
subTitleView = View.inflate(context, R.layout.subtitleview, null);
subChina = (SubtitleTextView) subTitleView.findViewById(R.id.subTitleChina);
subEnglish = (SubtitleTextView) subTitleView.findViewById(R.id.subTitleEnglish);
subChina.setSubtitleOnTouchListener(this);
subEnglish.setSubtitleOnTouchListener(this);
this.setOrientation(VERTICAL);
this.setGravity(Gravity.BOTTOM);
this.addView(subTitleView);
}
@Override
public void setItemSubtitleChina(String item)
{
subChina.setText(item);
}
@Override
public void setItemSubtitleEnglish(String item)
{
subEnglish.setText(item);
}
@Override
public void seekTo(int position)
{
if (data != null && !data.isEmpty())
{
model = searchSub(data, position);
Log.d("jixiongxu", position + "/" + data.get(data.size() - 1).end);
}
if (model != null)
{
setItemSubtitleChina(model.contextC);
setItemSubtitleEnglish(model.contextE);
}
else
{
setItemSubtitleChina("");
setItemSubtitleEnglish("");
}
}
@Override
public void setData(ArrayList<SubtitlesModel> list)
{
if (list == null || list.size() <= 0)
{
Log.e("jixiongxu", "subtitle data is empty");
return;
}
this.data = list;
}
@Override
public void setLanguage(int type)
{
if (type == LANGUAGE_TYPE_CHINA)
{
subChina.setVisibility(View.VISIBLE);
subEnglish.setVisibility(View.GONE);
}
else if (type == LANGUAGE_TYPE_ENGLISH)
{
subChina.setVisibility(View.GONE);
subEnglish.setVisibility(View.VISIBLE);
}
else if (type == LANGUAGE_TYPE_BOTH)
{
subChina.setVisibility(View.VISIBLE);
subEnglish.setVisibility(View.VISIBLE);
}
else
{
subChina.setVisibility(View.GONE);
subEnglish.setVisibility(View.GONE);
}
}
@Override
public void onWindowFocusChanged(boolean hasWindowFocus)
{
super.onWindowFocusChanged(hasWindowFocus);
Log.d("jixiongxu", "onWindowFocusChanged:" + hasWindowFocus);
}
@Override
public void setPause(boolean pause)
{
}
@Override
public void setStart(boolean start)
{
}
@Override
public void setStop(boolean stop)
{
}
@Override
public void setPlayOnBackground(boolean pb)
{
this.palyOnBackground = pb;
}
/**
* 采用二分法去查找当前应该播放的字幕
*
* @param list 全部字幕
* @param key 播放的时间点
* @return
*/
public static SubtitlesModel searchSub(ArrayList<SubtitlesModel> list, int key)
{
int start = 0;
int end = list.size() - 1;
while (start <= end)
{
int middle = (start + end) / 2;
if (key < list.get(middle).star)
{
if (key > list.get(middle).end)
{
return list.get(middle);
}
end = middle - 1;
}
else if (key > list.get(middle).end)
{
if (key < list.get(middle).star)
{
return list.get(middle);
}
start = middle + 1;
}
else if (key >= list.get(middle).star && key <= list.get(middle).end)
{
return list.get(middle);
}
}
return null;
}
@Override
public void ClickDown()
{
language++;
setLanguage(language % 3);
}
@Override
public void ClickUp()
{
}
}
SubtitleView的xml布局
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="bottom"
android:orientation="vertical">
<com.pplive.androidphone.layout.subtitle.SubtitleTextView
android:id="@+id/subTitleChina"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="30dp"
android:layout_marginRight="30dp"
android:gravity="center"
android:text="中文" />
<com.pplive.androidphone.layout.subtitle.SubtitleTextView
android:id="@+id/subTitleEnglish"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:layout_marginLeft="30dp"
android:layout_marginRight="30dp"
android:gravity="center"
android:text="English" />
</LinearLayout>
基本到这里就大功告成了,接下来就是使用了。
MainActivity使用
package demo.pplive.com.subtitles;
import android.Manifest;
import android.content.pm.PackageManager;
import android.os.Environment;
import android.support.v4.app.ActivityCompat;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import java.util.ArrayList;
public class MainActivity extends AppCompatActivity
{
private static final int REQUEST_EXTERNAL_STORAGE = 1;
private static String[] PERMISSIONS_STORAGE = {android.Manifest.permission.READ_EXTERNAL_STORAGE,
android.Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS};
private ArrayList<SubtitlesModel> list = new ArrayList<>();
private Button bt;
private EditText et;
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.layout);
list = SubtitlesCoding.getSubtitles();
final SubtitleView subtitleView = (SubtitleView) findViewById(R.id.subtitleview);
bt = (Button) findViewById(R.id.button);
et = (EditText) findViewById(R.id.edittext);
subtitleView.setData(list);
checkAndApplyPermission();
bt.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View view)
{
if (!et.getText().toString().isEmpty())
{
SubtitlesCoding.readFile(
Environment.getExternalStorageDirectory().getAbsolutePath() + et.getText().toString());
list = SubtitlesCoding.getSubtitles();
subtitleView.setData(list);
}
}
});
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults)
{
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
public void checkAndApplyPermission()
{
if (ActivityCompat.checkSelfPermission(this,
android.Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED)
{
ActivityCompat.requestPermissions(this, PERMISSIONS_STORAGE, REQUEST_EXTERNAL_STORAGE);
}
}
}
主界面的布局代码
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="#000"
android:focusable="true"
android:gravity="center">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitXY"
android:src="@drawable/player_view_bg2" />
<demo.pplive.com.subtitles.SubtitleView
android:id="@+id/subtitleview"
android:layout_width="match_parent"
android:layout_height="match_parent"></demo.pplive.com.subtitles.SubtitleView>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal">
<EditText
android:id="@+id/edittext"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="输入字幕位置"
android:textColor="#ffffff"
android:textColorHint="#ffffff" />
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="开始" />
</LinearLayout>
</FrameLayout>
使用的效果图如下
关于使用在本文的MainActivity没有体现出,而是直接seekTo()到某一时刻。在这里说一下。在使用过程中通过seekTo()函数就可以调用,也就是说视频播放过程中每隔一段时间就去seekTo()一次就可以与视频同步。视频播放一般都会有回调方法去返回当前播放的进度,而且是以毫秒为单位的。还有就是记得添加权限哟。如果大家有什么问题或改进建议也可以直接联系我。同时附上我的 gitup:https://github.com/jixiongxu/Subtitles.git 地址。