1 /******************************************************************************
2 QtAV: Multimedia framework based on Qt and FFmpeg
3 Copyright (C) 2012-2016 Wang Bin <wbsecg1@gmail.com>
4
5 * This file is part of QtAV (from 2014)
6
7 This library is free software; you can redistribute it and/or
8 modify it under the terms of the GNU Lesser General Public
9 License as published by the Free Software Foundation; either
10 version 2.1 of the License, or (at your option) any later version.
11
12 This library is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 Lesser General Public License for more details.
16
17 You should have received a copy of the GNU Lesser General Public
18 License along with this library; if not, write to the Free Software
19 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
20 ******************************************************************************/
21
22 #include "QtAV/private/PlayerSubtitle.h"
23 #include <QtCore/QDir>
24 #include <QtCore/QFileInfo>
25 #include "QtAV/AVPlayer.h"
26 #include "QtAV/Subtitle.h"
27 #include "utils/internal.h"
28 #include "utils/Logger.h"
29
30 namespace QtAV {
31
32 // /xx/oo/a.01.mov => /xx/oo/a.01. native dir separator => /
33 /*!
34 * \brief getSubtitleBasePath
35 * \param fullPath path of video or without extension
36 * \return absolute path without extension and file://
37 */
getSubtitleBasePath(const QString fullPath)38 static QString getSubtitleBasePath(const QString fullPath)
39 {
40 QString path(QDir::fromNativeSeparators(fullPath));
41 //path.remove(p->source().scheme() + "://");
42 // QString name = QFileInfo(path).completeBaseName();
43 // why QFileInfo(path).dir() starts with qml app dir?
44 QString name(path);
45 int lastSep = path.lastIndexOf(QLatin1Char('/'));
46 if (lastSep >= 0) {
47 name = name.mid(lastSep + 1);
48 path = path.left(lastSep + 1); // endsWidth "/"
49 }
50 int lastDot = name.lastIndexOf(QLatin1Char('.')); // not path.lastIndexof("."): xxx.oo/xx
51 if (lastDot > 0)
52 name = name.left(lastDot);
53 if (path.startsWith(QLatin1String("file:"))) // can skip convertion here. Subtitle class also convert the path
54 path = Internal::Path::toLocal(path);
55 path.append(name);
56 return path;
57 }
58
PlayerSubtitle(QObject * parent)59 PlayerSubtitle::PlayerSubtitle(QObject *parent)
60 : QObject(parent)
61 , m_auto(true)
62 , m_enabled(true)
63 , m_player(0)
64 , m_sub(new Subtitle(this))
65 {
66 }
67
subtitle()68 Subtitle* PlayerSubtitle::subtitle()
69 {
70 return m_sub;
71 }
72
setPlayer(AVPlayer * player)73 void PlayerSubtitle::setPlayer(AVPlayer *player)
74 {
75 if (m_player == player)
76 return;
77 if (m_player) {
78 disconnectSignals();
79 }
80 m_player = player;
81 if (!m_player)
82 return;
83 connectSignals();
84 }
85
setFile(const QString & file)86 void PlayerSubtitle::setFile(const QString &file)
87 {
88 if (m_file != file)
89 Q_EMIT fileChanged();
90 // always load
91 // file was set but now playing with fuzzy match. if file is set again to the same value, subtitle must load that file
92 m_file = file;
93 if (!m_enabled)
94 return;
95 m_sub->setFileName(file);
96 m_sub->setFuzzyMatch(false);
97 m_sub->loadAsync();
98 }
99
file() const100 QString PlayerSubtitle::file() const
101 {
102 return m_file;
103 }
setAutoLoad(bool value)104 void PlayerSubtitle::setAutoLoad(bool value)
105 {
106 if (m_auto == value)
107 return;
108 m_auto = value;
109 Q_EMIT autoLoadChanged(value);
110 }
111
autoLoad() const112 bool PlayerSubtitle::autoLoad() const
113 {
114 return m_auto;
115 }
116
onPlayerSourceChanged()117 void PlayerSubtitle::onPlayerSourceChanged()
118 {
119 if (!m_auto) {
120 m_sub->setFileName(QString());
121 return;
122 }
123 if (!m_enabled)
124 return;
125 AVPlayer *p = qobject_cast<AVPlayer*>(sender());
126 if (!p)
127 return;
128 m_sub->setFileName(getSubtitleBasePath(p->file()));
129 m_sub->setFuzzyMatch(true);
130 m_sub->loadAsync();
131 }
132
onPlayerPositionChanged()133 void PlayerSubtitle::onPlayerPositionChanged()
134 {
135 AVPlayer *p = qobject_cast<AVPlayer*>(sender());
136 if (!p)
137 return;
138 m_sub->setTimestamp(qreal(p->position())/1000.0);
139 }
140
onPlayerStart()141 void PlayerSubtitle::onPlayerStart()
142 {
143 if (!m_enabled)
144 return;
145 // priority: user file > auto load file > embedded
146 if (!m_file.isEmpty()) {
147 if (m_file == m_sub->fileName())
148 return;
149 m_sub->setFileName(m_file);
150 m_sub->setFuzzyMatch(false);
151 m_sub->loadAsync();
152 return;
153 }
154 if (autoLoad() && !m_sub->fileName().isEmpty())
155 return; //already loaded in onPlayerSourceChanged()
156 // try embedded subtitles
157 const int n = m_player->currentSubtitleStream();
158 if (n < 0 || m_tracks.isEmpty() || m_tracks.size() <= n) {
159 m_sub->processHeader(QByteArray(), QByteArray()); // reset
160 return;
161 }
162 QVariantMap track = m_tracks[n].toMap();
163 QByteArray codec(track.value(QStringLiteral("codec")).toByteArray());
164 QByteArray data(track.value(QStringLiteral("extra")).toByteArray());
165 m_sub->processHeader(codec, data);
166 return;
167 }
168
onEnabledChanged(bool value)169 void PlayerSubtitle::onEnabledChanged(bool value)
170 {
171 m_enabled = value;
172 if (!m_enabled) {
173 disconnectSignals();
174 return;
175 }
176 connectSignals();
177 // priority: user file > auto load file > embedded
178 if (!m_file.isEmpty()) {
179 if (m_sub->fileName() == m_file && m_sub->isLoaded())
180 return;
181 m_sub->setFileName(m_file);
182 m_sub->setFuzzyMatch(false);
183 m_sub->loadAsync();
184 }
185 if (!m_player)
186 return;
187 if (!autoLoad()) // fallback to internal subtitles
188 return;
189 m_sub->setFileName(getSubtitleBasePath(m_player->file()));
190 m_sub->setFuzzyMatch(true);
191 m_sub->loadAsync();
192 return;
193 }
194
tryReload()195 void PlayerSubtitle::tryReload()
196 {
197 tryReload(3);
198 }
199
tryReloadInternalSub()200 void PlayerSubtitle::tryReloadInternalSub()
201 {
202 tryReload(1);
203 }
204
tryReload(int flag)205 void PlayerSubtitle::tryReload(int flag)
206 {
207 if (!m_enabled)
208 return;
209 if (!m_player->isPlaying())
210 return;
211 const int kReloadExternal = 1<<1;
212 if (flag & kReloadExternal) {
213 //engine or charset changed
214 m_sub->processHeader(QByteArray(), QByteArray()); // reset
215 m_sub->loadAsync();
216 return;
217 }
218 // kReloadExternal is not set, kReloadInternal is unknown
219 //fallback to external sub if no valid internal sub track is set
220 const int n = m_player->currentSubtitleStream();
221 if (n < 0 || m_tracks.isEmpty() || m_tracks.size() <= n) {
222 m_sub->processHeader(QByteArray(), QByteArray()); // reset, null processor
223 m_sub->loadAsync();
224 return;
225 }
226 // try internal subtitles
227 QVariantMap track = m_tracks[n].toMap();
228 QByteArray codec(track.value(QStringLiteral("codec")).toByteArray());
229 QByteArray data(track.value(QStringLiteral("extra")).toByteArray());
230 m_sub->processHeader(codec, data);
231 Packet pkt(m_current_pkt[n]);
232 if (pkt.isValid()) {
233 processInternalSubtitlePacket(n, pkt);
234 }
235 }
236
updateInternalSubtitleTracks(const QVariantList & tracks)237 void PlayerSubtitle::updateInternalSubtitleTracks(const QVariantList &tracks)
238 {
239 m_tracks = tracks;
240 m_current_pkt.resize(tracks.size());
241 }
242
processInternalSubtitlePacket(int track,const QtAV::Packet & packet)243 void PlayerSubtitle::processInternalSubtitlePacket(int track, const QtAV::Packet &packet)
244 {
245 m_sub->processLine(packet.data, packet.pts, packet.duration);
246 m_current_pkt[track] = packet;
247 }
248
processInternalSubtitleHeader(const QByteArray & codec,const QByteArray & data)249 void PlayerSubtitle::processInternalSubtitleHeader(const QByteArray& codec, const QByteArray &data)
250 {
251 m_sub->processHeader(codec, data);
252 }
253
connectSignals()254 void PlayerSubtitle::connectSignals()
255 {
256 if (!m_player)
257 return;
258 connect(m_player, SIGNAL(sourceChanged()), this, SLOT(onPlayerSourceChanged()));
259 connect(m_player, SIGNAL(positionChanged(qint64)), this, SLOT(onPlayerPositionChanged()));
260 connect(m_player, SIGNAL(started()), this, SLOT(onPlayerStart()));
261 connect(m_player, SIGNAL(internalSubtitlePacketRead(int,QtAV::Packet)), this, SLOT(processInternalSubtitlePacket(int,QtAV::Packet)));
262 connect(m_player, SIGNAL(internalSubtitleHeaderRead(QByteArray,QByteArray)), this, SLOT(processInternalSubtitleHeader(QByteArray,QByteArray)));
263 connect(m_player, SIGNAL(internalSubtitleTracksChanged(QVariantList)), this, SLOT(updateInternalSubtitleTracks(QVariantList)));
264 // try to reload internal subtitle track. if failed and external subtitle is enabled, fallback to external
265 connect(m_player, SIGNAL(subtitleStreamChanged(int)), this, SLOT(tryReloadInternalSub()));
266 connect(m_sub, SIGNAL(codecChanged()), this, SLOT(tryReload()));
267 connect(m_sub, SIGNAL(enginesChanged()), this, SLOT(tryReload()));
268 }
269
disconnectSignals()270 void PlayerSubtitle::disconnectSignals()
271 {
272 if (!m_player)
273 return;
274 disconnect(m_player, SIGNAL(sourceChanged()), this, SLOT(onPlayerSourceChanged()));
275 disconnect(m_player, SIGNAL(positionChanged(qint64)), this, SLOT(onPlayerPositionChanged()));
276 disconnect(m_player, SIGNAL(started()), this, SLOT(onPlayerStart()));
277 disconnect(m_player, SIGNAL(internalSubtitlePacketRead(int,QtAV::Packet)), this, SLOT(processInternalSubtitlePacket(int,QtAV::Packet)));
278 disconnect(m_player, SIGNAL(internalSubtitleHeaderRead(QByteArray,QByteArray)), this, SLOT(processInternalSubtitleHeader(QByteArray,QByteArray)));
279 disconnect(m_player, SIGNAL(internalSubtitleTracksChanged(QVariantList)), this, SLOT(updateInternalSubtitleTracks(QVariantList)));
280 disconnect(m_sub, SIGNAL(codecChanged()), this, SLOT(tryReload()));
281 disconnect(m_sub, SIGNAL(enginesChanged()), this, SLOT(tryReload()));
282 }
283
284 } //namespace QtAV
285