26
2017
09

Android解析SRT字幕文件

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 地址。

上一篇:Dialog使用细节 下一篇:04 Activity窗口间的切换及参数的传递