Skip to content
GitLab
Menu
Projects
Groups
Snippets
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
e
os
android_packages_apps_Eleven
Commits
ad606c2e
Commit
ad606c2e
authored
Nov 19, 2014
by
linus_lee
Browse files
Eleven: Add Lyric (srt) support
https://cyanogen.atlassian.net/browse/MUSIC-186
Change-Id: I8fec1c61d69dca06be30ccebf9c996d7fac2ae75
parent
1f86c572
Changes
9
Hide whitespace changes
Inline
Side-by-side
res/layout/activity_player_fragment.xml
View file @
ad606c2e
...
...
@@ -49,6 +49,21 @@
android:clipChildren=
"false"
android:clipToPadding=
"false"
android:visibility=
"visible"
/>
<TextView
android:id=
"@+id/audio_player_lyrics"
android:layout_gravity=
"center"
android:gravity=
"center"
android:layout_width=
"fill_parent"
android:layout_height=
"wrap_content"
android:minHeight=
"66dp"
android:paddingLeft=
"15dp"
android:paddingRight=
"15dp"
android:paddingTop=
"6dp"
android:paddingBottom=
"6dp"
android:background=
"@color/lyrics_background_color"
android:textColor=
"@color/white"
android:textSize=
"@dimen/text_size_small"
/>
</com.cyngn.eleven.widgets.SquareFrame>
<RelativeLayout
...
...
res/values/colors.xml
View file @
ad606c2e
...
...
@@ -112,4 +112,7 @@
<color
name=
"widget_divider"
>
#373737
</color>
<!-- 80% opacity white -->
<color
name=
"widget_text"
>
#ccffffff
</color>
<!-- Background Lyrics Color -->
<color
name=
"lyrics_background_color"
>
#b2000000
</color>
</resources>
res/values/strings.xml
View file @
ad606c2e
...
...
@@ -138,6 +138,8 @@
<string
name=
"settings_download_artist_images_title"
>
Download missing artist images
</string>
<string
name=
"settings_general_category"
>
General
</string>
<string
name=
"settings_show_music_visualization_title"
>
Show music visualization
</string>
<string
name=
"settings_show_lyrics_title"
>
Show song lyrics
</string>
<string
name=
"settings_show_lyrics_summary"
>
For songs that have an srt file
</string>
<!-- App widget -->
<string
name=
"app_widget_small"
>
Music: 4 \u00d7 1
</string>
...
...
res/xml/settings.xml
View file @
ad606c2e
...
...
@@ -43,6 +43,13 @@
android:defaultValue=
"true"
android:key=
"music_visualization"
android:title=
"@string/settings_show_music_visualization_title"
/>
<!-- Show Lyrics -->
<CheckBoxPreference
android:defaultValue=
"true"
android:key=
"show_lyrics"
android:title=
"@string/settings_show_lyrics_title"
android:summary=
"@string/settings_show_lyrics_summary"
/>
</PreferenceCategory>
<!-- Storage catetory -->
<PreferenceCategory
android:title=
"@string/settings_storage_category"
>
...
...
src/com/cyngn/eleven/MusicPlaybackService.java
View file @
ad606c2e
...
...
@@ -61,7 +61,9 @@ import com.cyngn.eleven.provider.SongPlayCount;
import
com.cyngn.eleven.service.MusicPlaybackTrack
;
import
com.cyngn.eleven.utils.ApolloUtils
;
import
com.cyngn.eleven.utils.Lists
;
import
com.cyngn.eleven.utils.SrtManager
;
import
java.io.File
;
import
java.io.IOException
;
import
java.lang.ref.WeakReference
;
import
java.util.ArrayList
;
...
...
@@ -188,6 +190,11 @@ public class MusicPlaybackService extends Service {
*/
private
static
final
String
SHUTDOWN
=
"com.cyngn.eleven.shutdown"
;
/**
* Called to notify of a timed text
*/
public
static
final
String
NEW_LYRICS
=
"com.cyngn.eleven.lyrics"
;
/**
* Called to update the remote control client
*/
...
...
@@ -286,6 +293,11 @@ public class MusicPlaybackService extends Service {
*/
private
static
final
int
FADEUP
=
7
;
/**
* Notifies that there is a new timed text string
*/
private
static
final
int
LYRICS
=
8
;
/**
* Idle time before stopping the foreground notfication (5 minutes)
*/
...
...
@@ -450,6 +462,8 @@ public class MusicPlaybackService extends Service {
private
int
mServiceStartId
=
-
1
;
private
String
mLyrics
;
private
ArrayList
<
MusicPlaybackTrack
>
mPlaylist
=
new
ArrayList
<
MusicPlaybackTrack
>(
100
);
private
long
[]
mAutoShuffleList
=
null
;
...
...
@@ -1373,6 +1387,11 @@ public class MusicPlaybackService extends Service {
intent
.
putExtra
(
"album"
,
getAlbumName
());
intent
.
putExtra
(
"track"
,
getTrackName
());
intent
.
putExtra
(
"playing"
,
isPlaying
());
if
(
NEW_LYRICS
.
equals
(
what
))
{
intent
.
putExtra
(
"lyrics"
,
mLyrics
);
}
sendStickyBroadcast
(
intent
);
final
Intent
musicIntent
=
new
Intent
(
intent
);
...
...
@@ -2682,6 +2701,10 @@ public class MusicPlaybackService extends Service {
service
.
gotoNext
(
false
);
}
break
;
case
LYRICS:
service
.
mLyrics
=
(
String
)
msg
.
obj
;
service
.
notifyChange
(
NEW_LYRICS
);
break
;
case
RELEASE_WAKELOCK:
service
.
mWakeLock
.
release
();
break
;
...
...
@@ -2781,12 +2804,22 @@ public class MusicPlaybackService extends Service {
private
boolean
mIsInitialized
=
false
;
private
SrtManager
mSrtManager
;
private
String
mNextMediaPath
;
/**
* Constructor of <code>MultiPlayer</code>
*/
public
MultiPlayer
(
final
MusicPlaybackService
service
)
{
mService
=
new
WeakReference
<
MusicPlaybackService
>(
service
);
mCurrentMediaPlayer
.
setWakeMode
(
mService
.
get
(),
PowerManager
.
PARTIAL_WAKE_LOCK
);
mSrtManager
=
new
SrtManager
()
{
@Override
public
void
onTimedText
(
String
text
)
{
mHandler
.
obtainMessage
(
LYRICS
,
text
).
sendToTarget
();
}
};
}
/**
...
...
@@ -2796,10 +2829,48 @@ public class MusicPlaybackService extends Service {
public
void
setDataSource
(
final
String
path
)
{
mIsInitialized
=
setDataSourceImpl
(
mCurrentMediaPlayer
,
path
);
if
(
mIsInitialized
)
{
loadSrt
(
path
);
setNextDataSource
(
null
);
}
}
private
void
loadSrt
(
final
String
path
)
{
mSrtManager
.
reset
();
Uri
uri
=
Uri
.
parse
(
path
);
String
filePath
=
null
;
if
(
path
.
startsWith
(
"content://"
))
{
// resolve the content resolver path to a file path
Cursor
cursor
=
null
;
try
{
final
String
[]
proj
=
{
MediaStore
.
Audio
.
Media
.
DATA
};
cursor
=
mService
.
get
().
getContentResolver
().
query
(
uri
,
proj
,
null
,
null
,
null
);
if
(
cursor
!=
null
&&
cursor
.
moveToFirst
())
{
filePath
=
cursor
.
getString
(
0
);
}
}
finally
{
if
(
cursor
!=
null
)
{
cursor
.
close
();
cursor
=
null
;
}
}
}
else
{
filePath
=
uri
.
getPath
();
}
if
(!
TextUtils
.
isEmpty
(
filePath
))
{
final
int
lastIndex
=
filePath
.
lastIndexOf
(
'.'
);
if
(
lastIndex
!=
-
1
)
{
String
newPath
=
filePath
.
substring
(
0
,
lastIndex
)
+
".srt"
;
final
File
f
=
new
File
(
newPath
);
mSrtManager
.
initialize
(
mCurrentMediaPlayer
,
f
);
}
}
}
/**
* @param player The {@link MediaPlayer} to use
* @param path The path of the file, or the http/rtsp URL of the stream
...
...
@@ -2817,6 +2888,7 @@ public class MusicPlaybackService extends Service {
player
.
setDataSource
(
path
);
}
player
.
setAudioStreamType
(
AudioManager
.
STREAM_MUSIC
);
player
.
prepare
();
}
catch
(
final
IOException
todo
)
{
// TODO: notify the user why the file couldn't be opened
...
...
@@ -2841,6 +2913,7 @@ public class MusicPlaybackService extends Service {
* you want to play
*/
public
void
setNextDataSource
(
final
String
path
)
{
mNextMediaPath
=
null
;
try
{
mCurrentMediaPlayer
.
setNextMediaPlayer
(
null
);
}
catch
(
IllegalArgumentException
e
)
{
...
...
@@ -2860,6 +2933,7 @@ public class MusicPlaybackService extends Service {
mNextMediaPlayer
.
setWakeMode
(
mService
.
get
(),
PowerManager
.
PARTIAL_WAKE_LOCK
);
mNextMediaPlayer
.
setAudioSessionId
(
getAudioSessionId
());
if
(
setDataSourceImpl
(
mNextMediaPlayer
,
path
))
{
mNextMediaPath
=
path
;
mCurrentMediaPlayer
.
setNextMediaPlayer
(
mNextMediaPlayer
);
}
else
{
if
(
mNextMediaPlayer
!=
null
)
{
...
...
@@ -2890,6 +2964,7 @@ public class MusicPlaybackService extends Service {
*/
public
void
start
()
{
mCurrentMediaPlayer
.
start
();
mSrtManager
.
play
();
}
/**
...
...
@@ -2897,6 +2972,7 @@ public class MusicPlaybackService extends Service {
*/
public
void
stop
()
{
mCurrentMediaPlayer
.
reset
();
mSrtManager
.
reset
();
mIsInitialized
=
false
;
}
...
...
@@ -2906,6 +2982,8 @@ public class MusicPlaybackService extends Service {
public
void
release
()
{
stop
();
mCurrentMediaPlayer
.
release
();
mSrtManager
.
release
();
mSrtManager
=
null
;
}
/**
...
...
@@ -2913,6 +2991,7 @@ public class MusicPlaybackService extends Service {
*/
public
void
pause
()
{
mCurrentMediaPlayer
.
pause
();
mSrtManager
.
pause
();
}
/**
...
...
@@ -2941,6 +3020,7 @@ public class MusicPlaybackService extends Service {
*/
public
long
seek
(
final
long
whereto
)
{
mCurrentMediaPlayer
.
seekTo
((
int
)
whereto
);
mSrtManager
.
seekTo
(
whereto
);
return
whereto
;
}
...
...
@@ -2998,6 +3078,8 @@ public class MusicPlaybackService extends Service {
if
(
mp
==
mCurrentMediaPlayer
&&
mNextMediaPlayer
!=
null
)
{
mCurrentMediaPlayer
.
release
();
mCurrentMediaPlayer
=
mNextMediaPlayer
;
loadSrt
(
mNextMediaPath
);
mNextMediaPath
=
null
;
mNextMediaPlayer
=
null
;
mHandler
.
sendEmptyMessage
(
TRACK_WENT_TO_NEXT
);
}
else
{
...
...
src/com/cyngn/eleven/ui/fragments/AudioPlayerFragment.java
View file @
ad606c2e
...
...
@@ -16,6 +16,9 @@ import android.os.IBinder;
import
android.os.Message
;
import
android.support.v4.app.Fragment
;
import
android.support.v4.view.ViewPager
;
import
android.text.Html
;
import
android.text.Spanned
;
import
android.text.TextUtils
;
import
android.util.Log
;
import
android.view.ContextMenu
;
import
android.view.LayoutInflater
;
...
...
@@ -133,6 +136,9 @@ public class AudioPlayerFragment extends Fragment implements ServiceConnection,
// popup menu for pressing the menu icon
private
PopupMenu
mPopupMenu
;
// Lyrics text view
private
TextView
mLyricsText
;
private
long
mSelectedId
=
-
1
;
private
boolean
mIsPaused
=
false
;
...
...
@@ -180,6 +186,8 @@ public class AudioPlayerFragment extends Fragment implements ServiceConnection,
mEqualizerView
.
initialize
(
getActivity
());
mEqualizerGradient
=
mRootView
.
findViewById
(
R
.
id
.
equalizerGradient
);
mLyricsText
=
(
TextView
)
mRootView
.
findViewById
(
R
.
id
.
audio_player_lyrics
);
return
mRootView
;
}
...
...
@@ -216,6 +224,8 @@ public class AudioPlayerFragment extends Fragment implements ServiceConnection,
filter
.
addAction
(
MusicPlaybackService
.
REFRESH
);
// Listen to changes to the entire queue
filter
.
addAction
(
MusicPlaybackService
.
QUEUE_CHANGED
);
// Listen for lyrics text for the audio track
filter
.
addAction
(
MusicPlaybackService
.
NEW_LYRICS
);
// Register the intent filters
getActivity
().
registerReceiver
(
mPlaybackStatus
,
filter
);
// Refresh the current time
...
...
@@ -685,6 +695,19 @@ public class AudioPlayerFragment extends Fragment implements ServiceConnection,
return
super
.
onContextItemSelected
(
item
);
}
public
void
onLyrics
(
String
lyrics
)
{
if
(
TextUtils
.
isEmpty
(
lyrics
)
||
!
PreferenceUtils
.
getInstance
(
getActivity
()).
getShowLyrics
())
{
mLyricsText
.
animate
().
alpha
(
0
).
setDuration
(
200
);
}
else
{
lyrics
=
lyrics
.
replace
(
"\n"
,
"<br/>"
);
Spanned
span
=
Html
.
fromHtml
(
lyrics
);
mLyricsText
.
setText
(
span
);
mLyricsText
.
animate
().
alpha
(
1
).
setDuration
(
200
);
}
}
@Override
public
void
onBeginSlide
()
{
mEqualizerView
.
setPanelVisible
(
false
);
...
...
@@ -743,25 +766,28 @@ public class AudioPlayerFragment extends Fragment implements ServiceConnection,
*/
@Override
public
void
onReceive
(
final
Context
context
,
final
Intent
intent
)
{
final
AudioPlayerFragment
audioPlayerFragment
=
mReference
.
get
();
final
String
action
=
intent
.
getAction
();
if
(
action
.
equals
(
MusicPlaybackService
.
META_CHANGED
))
{
// Current info
mReference
.
get
()
.
updateNowPlayingInfo
();
mReference
.
get
()
.
dismissPopupMenu
();
audioPlayerFragment
.
updateNowPlayingInfo
();
audioPlayerFragment
.
dismissPopupMenu
();
}
else
if
(
action
.
equals
(
MusicPlaybackService
.
PLAYSTATE_CHANGED
))
{
// Set the play and pause image
mReference
.
get
()
.
mPlayPauseProgressButton
.
getPlayPauseButton
().
updateState
();
audioPlayerFragment
.
mPlayPauseProgressButton
.
getPlayPauseButton
().
updateState
();
}
else
if
(
action
.
equals
(
MusicPlaybackService
.
REPEATMODE_CHANGED
)
||
action
.
equals
(
MusicPlaybackService
.
SHUFFLEMODE_CHANGED
))
{
// Set the repeat image
mReference
.
get
()
.
mRepeatButton
.
updateRepeatState
();
audioPlayerFragment
.
mRepeatButton
.
updateRepeatState
();
// Set the shuffle image
mReference
.
get
()
.
mShuffleButton
.
updateShuffleState
();
audioPlayerFragment
.
mShuffleButton
.
updateShuffleState
();
// Update the queue
mReference
.
get
()
.
createAndSetAdapter
();
audioPlayerFragment
.
createAndSetAdapter
();
}
else
if
(
action
.
equals
(
MusicPlaybackService
.
QUEUE_CHANGED
))
{
mReference
.
get
().
createAndSetAdapter
();
audioPlayerFragment
.
createAndSetAdapter
();
}
else
if
(
action
.
equals
(
MusicPlaybackService
.
NEW_LYRICS
))
{
audioPlayerFragment
.
onLyrics
(
intent
.
getStringExtra
(
"lyrics"
));
}
}
}
...
...
src/com/cyngn/eleven/utils/PreferenceUtils.java
View file @
ad606c2e
...
...
@@ -69,6 +69,9 @@ public final class PreferenceUtils {
// datetime cutoff for determining which songs go in last added playlist
public
static
final
String
LAST_ADDED_CUTOFF
=
"last_added_cutoff"
;
// show lyrics option
public
static
final
String
SHOW_LYRICS
=
"show_lyrics"
;
// show visualizer flag
public
static
final
String
SHOW_VISUALIZER
=
"music_visualization"
;
...
...
@@ -307,6 +310,13 @@ public final class PreferenceUtils {
return
mPreferences
.
getLong
(
LAST_ADDED_CUTOFF
,
0L
);
}
/**
* @return Whether we want to show lyrics
*/
public
final
boolean
getShowLyrics
()
{
return
mPreferences
.
getBoolean
(
SHOW_LYRICS
,
true
);
}
public
boolean
getShowVisualizer
()
{
return
mPreferences
.
getBoolean
(
SHOW_VISUALIZER
,
true
);
}
...
...
src/com/cyngn/eleven/utils/SrtManager.java
0 → 100644
View file @
ad606c2e
/*
* Copyright (C) 2014 Cyanogen, Inc.
*/
package
com.cyngn.eleven.utils
;
import
android.media.MediaPlayer
;
import
android.os.Handler
;
import
android.os.HandlerThread
;
import
android.os.Message
;
import
android.util.Log
;
import
java.io.File
;
import
java.util.ArrayList
;
/**
* Class that helps signal when srt text comes and goes
*/
public
abstract
class
SrtManager
implements
Handler
.
Callback
{
private
static
final
String
TAG
=
SrtManager
.
class
.
getSimpleName
();
private
static
final
boolean
DEBUG
=
false
;
private
static
final
int
POST_TEXT_MSG
=
0
;
private
ArrayList
<
SrtParser
.
SrtEntry
>
mEntries
;
private
Handler
mHandler
;
private
HandlerThread
mHandlerThread
;
private
Runnable
mLoader
;
private
MediaPlayer
mMediaPlayer
;
private
int
mNextIndex
;
public
SrtManager
()
{
mHandlerThread
=
new
HandlerThread
(
"SrtManager"
,
android
.
os
.
Process
.
THREAD_PRIORITY_FOREGROUND
);
mHandlerThread
.
start
();
mHandler
=
new
Handler
(
mHandlerThread
.
getLooper
(),
this
);
}
public
synchronized
void
reset
()
{
mHandler
.
removeMessages
(
POST_TEXT_MSG
);
mHandler
.
removeCallbacks
(
mLoader
);
mEntries
=
null
;
mLoader
=
null
;
mMediaPlayer
=
null
;
mNextIndex
=
-
1
;
// post a null timed text to clear
onTimedText
(
null
);
}
public
synchronized
void
release
()
{
reset
();
mHandlerThread
.
quit
();
mHandlerThread
=
null
;
}
@Override
protected
void
finalize
()
throws
Throwable
{
super
.
finalize
();
mHandlerThread
.
quit
();
mHandlerThread
=
null
;
}
public
synchronized
void
initialize
(
final
MediaPlayer
player
,
final
File
f
)
{
if
(
player
==
null
||
f
==
null
)
{
throw
new
IllegalArgumentException
(
"Must have a valid player and file"
);
}
reset
();
if
(!
f
.
exists
())
{
return
;
}
mMediaPlayer
=
player
;
mLoader
=
new
Runnable
()
{
@Override
public
void
run
()
{
onLoaded
(
this
,
SrtParser
.
getSrtEntries
(
f
));
}
};
mHandler
.
post
(
mLoader
);
}
public
synchronized
void
seekTo
(
long
timeMs
)
{
mHandler
.
removeMessages
(
POST_TEXT_MSG
);
mNextIndex
=
0
;
if
(
mEntries
!=
null
)
{
if
(
DEBUG
)
{
Log
.
d
(
TAG
,
"Seeking to: "
+
timeMs
);
}
// find the first entry after the current time and set mNextIndex to the one before that
for
(
int
i
=
0
;
i
<
mEntries
.
size
();
i
++)
{
mNextIndex
=
i
;
if
(
i
+
1
<
mEntries
.
size
()
&&
mEntries
.
get
(
i
+
1
).
mStartTimeMs
>
timeMs
)
{
break
;
}
}
postNextTimedText
();
}
}
public
synchronized
void
pause
()
{
mHandler
.
removeMessages
(
POST_TEXT_MSG
);
}
public
synchronized
void
play
()
{
postNextTimedText
();
}
private
synchronized
void
onLoaded
(
Runnable
r
,
ArrayList
<
SrtParser
.
SrtEntry
>
entries
)
{
// if this is the same loader
if
(
r
==
mLoader
)
{
mEntries
=
entries
;
if
(
mEntries
!=
null
)
{
if
(
DEBUG
)
{
Log
.
d
(
TAG
,
"Loaded: "
+
entries
.
size
()
+
" number of entries"
);
}
try
{
seekTo
(
mMediaPlayer
.
getCurrentPosition
());
}
catch
(
IllegalStateException
e
)
{
Log
.
d
(
TAG
,
"illegal state but failing silently"
);
reset
();
}
}
}
}
private
synchronized
void
postNextTimedText
()
{
if
(
mEntries
!=
null
)
{
long
timeMs
=
0
;
try
{
timeMs
=
mMediaPlayer
.
getCurrentPosition
();
}
catch
(
IllegalStateException
e
)
{
Log
.
d
(
TAG
,
"illegal state - probably because media player has been "
+
"stopped/released. failing silently"
);
return
;
}
String
currentMessage
=
null
;
long
targetTime
=
-
1
;
// shift mNextIndex until it hits the next item we want
while
(
mNextIndex
<
mEntries
.
size
()
&&
mEntries
.
get
(
mNextIndex
).
mStartTimeMs
<
timeMs
)
{
mNextIndex
++;
}
// if the previous entry is valid, set the message and target time
if
(
mNextIndex
>
0
&&
entrySurroundsTime
(
mEntries
.
get
(
mNextIndex
-
1
),
timeMs
))
{
currentMessage
=
mEntries
.
get
(
mNextIndex
-
1
).
mLine
;
targetTime
=
mEntries
.
get
(
mNextIndex
-
1
).
mEndTimeMs
;
}
onTimedText
(
currentMessage
);
// if our next index is valid, and we don't have a target time, set it
if
(
mNextIndex
<
mEntries
.
size
()
&&
targetTime
==
-
1
)
{
targetTime
=
mEntries
.
get
(
mNextIndex
).
mStartTimeMs
;
}
// if we have a targeted time entry and we are playing, then queue up a delayed message
if
(
targetTime
>=
0
&&
mMediaPlayer
.
isPlaying
())
{
mHandler
.
removeMessages
(
POST_TEXT_MSG
);
long
delay
=
targetTime
-
timeMs
;
mHandler
.
sendEmptyMessageDelayed
(
POST_TEXT_MSG
,
delay
);
if
(
DEBUG
&&
mNextIndex
<
mEntries
.
size
())
{
Log
.
d
(
TAG
,
"Preparing next message: "
+
delay
+
"ms from now with msg: "
+
mEntries
.
get
(
mNextIndex
).
mLine
);
}
}
}
}
@Override
public
boolean
handleMessage
(
Message
msg
)
{
switch
(
msg
.
what
)
{
case
POST_TEXT_MSG:
postNextTimedText
();
return
true
;
}
return
false
;
}
private
static
boolean
entrySurroundsTime
(
SrtParser
.
SrtEntry
entry
,
long
time
)
{
return
entry
.
mStartTimeMs
<=
time
&&
entry
.
mEndTimeMs
>=
time
;
}
public
abstract
void
onTimedText
(
String
txt
);
}
src/com/cyngn/eleven/utils/SrtParser.java
0 → 100644
View file @
ad606c2e
/*
* Copyright (C) 2014 Cyanogen, Inc.
*/
package
com.cyngn.eleven.utils
;
import
android.text.TextUtils
;
import
android.util.Log
;
import
java.io.BufferedReader
;
import
java.io.File
;
import
java.io.FileReader
;
import
java.io.IOException
;
import
java.util.ArrayList
;
public
class
SrtParser
{
private
static
final
String
TAG
=
SrtParser
.
class
.
getSimpleName
();
public
static
class
SrtEntry
{
public
long
mStartTimeMs
;
public
long
mEndTimeMs
;
String
mLine
;
}
/**
* The SubRip file format should contain entries that follow the following format:
*
* 1. A numeric counter identifying each sequential subtitle
* 2. The time that the subtitle should appear on the screen, followed by --> and the time it
* should disappear
* 3. Subtitle text itself on one or more lines
* 4. A blank line containing no text, indicating the end of this subtitle
*