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