1
# Miro - an RSS based video player application
2
# Copyright (C) 2005-2010 Participatory Culture Foundation
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 of the License, or
7
# (at your option) any later version.
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
# GNU General Public License for more details.
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
18
# In addition, as a special exception, the copyright holders give
19
# permission to link the code of portions of this program with the OpenSSL
22
# You must obey the GNU General Public License in all respects for all of
23
# the code used other than OpenSSL. If you modify file(s) with this
24
# exception, you may extend this exception to your version of the file(s),
25
# but you are not obligated to do so. If you do not wish to do so, delete
26
# this exception statement from your version. If you delete this exception
27
# statement from all source files in the program, then also delete it here.
33
from objc import pathForFramework, loadBundleFunctions
34
from Foundation import *
38
from miro import prefs
39
from miro import config
40
from miro import player
41
from miro import iso_639
42
from miro.plat import utils
43
from miro.plat import bundle
44
from miro.plat import qtcomp
45
from miro.plat import script_codes
46
from miro.plat.frontends.widgets import threads
47
from miro.plat.frontends.widgets.helpers import NotificationForwarder
48
from miro.util import copy_subtitle_file
50
###############################################################################
52
script_codes.patch_iso_639_map()
54
###############################################################################
56
qt_framework = pathForFramework("QuickTime.framework")
57
qt_bundle = NSBundle.bundleWithPath_(qt_framework)
58
loadBundleFunctions(qt_bundle, globals(), (('GetMediaLanguage', 's^^{}'),))
60
###############################################################################
62
def register_components():
63
bundlePath = bundle.getBundlePath()
64
componentsDirectoryPath = os.path.join(bundlePath, 'Contents', 'Components')
65
components = glob.glob(os.path.join(componentsDirectoryPath, '*.component'))
66
for component in components:
67
cmpName = os.path.basename(component)
68
ok = qtcomp.register(component.encode('utf-8'))
70
logging.info('Successfully registered embedded component: %s' % cmpName)
72
logging.warn('Error while registering embedded component: %s' % cmpName)
74
###############################################################################
76
class WarmupProgressHandler(NSObject):
78
self = super(WarmupProgressHandler, self).init()
81
def loadStateChanged_(self, notification):
82
self.handleLoadStateForMovie_(notification.object())
83
def handleInitialLoadStateForMovie_(self, movie):
84
load_state = movie.attributeForKey_(QTMovieLoadStateAttribute).longValue()
85
if load_state < QTMovieLoadStateComplete:
86
NSNotificationCenter.defaultCenter().addObserver_selector_name_object_(
89
QTMovieLoadStateDidChangeNotification,
92
self.handleLoadStateForMovie_(movie)
93
def handleLoadStateForMovie_(self, movie):
94
load_state = movie.attributeForKey_(QTMovieLoadStateAttribute).longValue()
95
if load_state == QTMovieLoadStateComplete:
96
logging.info("QuickTime warm up complete")
97
NSNotificationCenter.defaultCenter().removeObserver_(self)
98
NSTimer.scheduledTimerWithTimeInterval_target_selector_userInfo_repeats_(
99
10, self, 'releaseWarmupMovie:', None, False)
101
def releaseWarmupMovie_(self, timer):
102
logging.info("Releasing warmup movie.")
106
warmup_handler = WarmupProgressHandler.alloc().init()
110
logging.info('Warming up QuickTime')
111
rsrcPath = bundle.getBundleResourcePath()
113
attributes = NSMutableDictionary.dictionary()
114
attributes['QTMovieFileNameAttribute'] = os.path.join(rsrcPath, 'warmup.mov')
115
attributes['QTMovieOpenAsyncRequiredAttribute'] = True
116
attributes['QTMovieDelegateAttribute'] = None
119
if utils.get_pyobjc_major_version() == 2:
120
warmup_movie, error = QTMovie.movieWithAttributes_error_(attributes, None)
122
warmup_movie, error = QTMovie.movieWithAttributes_error_(attributes)
124
if error is not None:
125
logging.warn("QuickTime Warm Up failed: %s" % error)
127
warmup_handler.handleInitialLoadStateForMovie_(warmup_movie)
129
###############################################################################
131
def qttime2secs(qttime):
132
timeScale = qttimescale(qttime)
135
timeValue = qttimevalue(qttime)
136
return timeValue / float(timeScale)
138
def qttimescale(qttime):
139
if isinstance(qttime, tuple):
142
return qttime.timeScale
144
def qttimevalue(qttime):
145
if isinstance(qttime, tuple):
148
return qttime.timeValue
150
###############################################################################
151
# The QTMediaTypeSubtitle and QTMediaTypeClosedCaption media types are only
152
# available in OS X 10.6 so we emulate them if they aren't defined.
155
dummy = QTMediaTypeSubtitle
157
QTMediaTypeSubtitle = u'sbtl'
160
dummy = QTMediaTypeClosedCaption
162
QTMediaTypeClosedCaption = u'clcp'
164
###############################################################################
166
class Player(player.Player):
168
def __init__(self, supported_media_types):
169
player.Player.__init__(self)
170
self.supported_media_types = supported_media_types
171
self.movie_notifications = None
173
self.item_info = None
176
threads.warn_if_not_on_main_thread('quicktime.Player.reset')
177
if self.movie_notifications is not None:
178
self.movie_notifications.disconnect()
179
self.movie_notifications = None
181
self.item_info = None
183
def set_item(self, item_info, callback, errback, force_subtitles=False):
184
threads.warn_if_not_on_main_thread('quicktime.Player.set_item')
185
qtmovie = self.get_movie_from_file(item_info.video_path)
187
if qtmovie is not None:
189
self.item_info = item_info
190
self.movie_notifications = NotificationForwarder.create(self.movie)
191
self.movie_notifications.connect(self.handle_movie_notification, QTMovieDidEndNotification)
192
self.setup_subtitles(force_subtitles)
197
def get_movie_from_file(self, path):
198
osfilename = utils.filename_type_to_os_filename(path)
199
url = NSURL.fileURLWithPath_(osfilename)
200
if utils.get_pyobjc_major_version() == 2:
201
qtmovie, error = QTMovie.movieWithURL_error_(url, None)
203
qtmovie, error = QTMovie.movieWithURL_error_(url)
204
if qtmovie is None or error is not None:
206
if not self.can_open_file(qtmovie):
210
def can_open_file(self, qtmovie):
211
threads.warn_if_not_on_main_thread('quicktime.Player.can_open_file')
213
duration = utils.qttimevalue(qtmovie.duration())
215
if qtmovie is not None and duration > 0:
216
allTracks = qtmovie.tracks()
217
if len(qtmovie.tracks()) > 0:
218
# Make sure we have at least one track with a non zero length
219
allMedia = [track.media() for track in allTracks]
220
for media in allMedia:
221
mediaType = media.attributeForKey_(QTMediaTypeAttribute)
222
mediaDuration = utils.qttimevalue(media.attributeForKey_(QTMediaDurationAttribute).QTTimeValue())
223
if mediaType in self.supported_media_types and mediaDuration > 0:
229
def setup_subtitles(self, force_subtitles):
230
if config.get(prefs.ENABLE_SUBTITLES) or force_subtitles:
231
default_track = self.get_enabled_subtitle_track()
232
if default_track is None:
233
tracks = self.get_subtitle_tracks()
235
self.enable_subtitle_track(tracks[0])
237
self.disable_subtitles()
239
def get_subtitle_tracks(self):
241
if self.movie is not None:
242
for track in self.movie.tracks():
243
if self.is_subtitle_track(track):
244
media = track.media().quickTimeMedia()
245
lang = GetMediaLanguage(media)
246
display_name = track.attributeForKey_(QTTrackDisplayNameAttribute)
247
if lang == 32767: # 32764 = langUndefined
250
lang_info = iso_639.find(lang, "script-code")
251
if lang_info is None:
254
name = lang_info["name"]
255
is_enabled = track.attributeForKey_(QTTrackEnabledAttribute) == 1
256
track_id = track.attributeForKey_(QTTrackIDAttribute)
257
tracks.append((track_id, name, is_enabled))
260
def _find_track(self, key, value):
261
if self.movie is not None:
262
for track in self.movie.tracks():
263
if self.is_subtitle_track(track):
264
if track.attributeForKey_(key) == value:
268
def get_enabled_subtitle_track(self):
269
return self._find_track(QTTrackEnabledAttribute, 1)
271
def get_subtitle_track_by_name(self, name):
272
return self._find_track(QTTrackDisplayNameAttribute, name)
274
def get_subtitle_track_by_id(self, track_id):
275
return self._find_track(QTTrackIDAttribute, track_id)
277
def is_subtitle_track(self, track):
278
layer = track.attributeForKey_(QTTrackLayerAttribute)
279
media_type = track.attributeForKey_(QTTrackMediaTypeAttribute)
280
return (layer == -1 and media_type == QTMediaTypeVideo) or media_type in [QTMediaTypeSubtitle, QTMediaTypeClosedCaption]
282
def enable_subtitle_track(self, track_id):
283
current = self.get_enabled_subtitle_track()
284
if current is not None:
285
current.setAttribute_forKey_(0, QTTrackEnabledAttribute)
286
to_enable = self.get_subtitle_track_by_id(track_id)
287
if to_enable is not None:
288
to_enable.setAttribute_forKey_(1, QTTrackEnabledAttribute)
290
def disable_subtitles(self):
291
track = self.get_enabled_subtitle_track()
292
if track is not None:
293
track.setAttribute_forKey_(0, QTTrackEnabledAttribute)
295
def select_subtitle_file(self, sub_path, handle_successful_select):
297
handle_successful_select()
299
app.playback_manager.stop()
300
copy_subtitle_file(sub_path, self.item_info.video_path)
301
self.set_item(self.item_info, handle_ok, handle_err, True)
303
def set_volume(self, volume):
305
self.movie.setVolume_(volume)
307
def get_elapsed_playback_time(self):
308
qttime = self.movie.currentTime()
309
return utils.qttime2secs(qttime)
311
def get_total_playback_time(self):
312
if self.movie is None:
314
qttime = self.movie.duration()
315
return utils.qttime2secs(qttime)
317
def skip_forward(self):
318
current = self.get_elapsed_playback_time()
319
duration = self.get_total_playback_time()
320
pos = min(duration, current + 30.0)
321
self.seek_to(pos / duration)
323
def skip_backward(self):
324
current = self.get_elapsed_playback_time()
325
duration = self.get_total_playback_time()
326
pos = max(0, current - 15.0)
327
self.seek_to(pos / duration)
329
def seek_to(self, position):
330
qttime = self.movie.duration()
331
if isinstance(qttime, tuple):
332
qttime = (qttime[0] * position, qttime[1], qttime[2])
334
qttime.timeValue = qttime.timeValue * position
335
self.movie.setCurrentTime_(qttime)
337
def play_from_time(self, resume_time=0):
338
self.seek_to(resume_time / self.get_total_playback_time())
341
def set_playback_rate(self, rate):
342
self.movie.setRate_(rate)
344
def handle_movie_notification(self, notification):
345
if notification.name() == QTMovieDidEndNotification and not app.playback_manager.is_suspended:
346
app.playback_manager.on_movie_finished()
348
###############################################################################