~ubuntu-branches/ubuntu/natty/miro/natty

« back to all changes in this revision

Viewing changes to osx/plat/frontends/widgets/quicktime.py

  • Committer: Bazaar Package Importer
  • Author(s): Bryce Harrington
  • Date: 2011-01-22 02:46:33 UTC
  • mfrom: (1.4.10 upstream) (1.7.5 experimental)
  • Revision ID: james.westby@ubuntu.com-20110122024633-kjme8u93y2il5nmf
Tags: 3.5.1-1ubuntu1
* Merge from debian.  Remaining ubuntu changes:
  - Use python 2.7 instead of python 2.6
  - Relax dependency on python-dbus to >= 0.83.0

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Miro - an RSS based video player application
 
2
# Copyright (C) 2005-2010 Participatory Culture Foundation
 
3
#
 
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.
 
8
#
 
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.
 
13
#
 
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
 
17
#
 
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
 
20
# library.
 
21
#
 
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.
 
28
 
 
29
import os
 
30
import glob
 
31
import logging
 
32
 
 
33
from objc import pathForFramework, loadBundleFunctions
 
34
from Foundation import *
 
35
from QTKit import *
 
36
 
 
37
from miro import app
 
38
from miro import prefs
 
39
from miro import config
 
40
from miro import player
 
41
from miro import iso639
 
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
 
49
 
 
50
###############################################################################
 
51
 
 
52
qt_framework = pathForFramework("QuickTime.framework")
 
53
qt_bundle = NSBundle.bundleWithPath_(qt_framework)
 
54
loadBundleFunctions(qt_bundle, globals(), (('GetMediaLanguage', 's^^{}'),))
 
55
 
 
56
###############################################################################
 
57
 
 
58
def register_components():
 
59
    bundlePath = bundle.getBundlePath()
 
60
    componentsDirectoryPath = os.path.join(bundlePath, 'Contents', 'Components')
 
61
    components = glob.glob(os.path.join(componentsDirectoryPath, '*.component'))
 
62
    for component in components:
 
63
        cmpName = os.path.basename(component)
 
64
        stdloc1 = os.path.join("/", "Library", "Quicktime", cmpName)
 
65
        stdloc2 = os.path.join("/", "Library", "Audio", "Plug-Ins", "Components", cmpName)
 
66
        if not os.path.exists(stdloc1) and not os.path.exists(stdloc2):
 
67
            ok = qtcomp.register(component.encode('utf-8'))
 
68
            if ok:
 
69
                logging.info('Successfully registered embedded component: %s' % cmpName)
 
70
            else:
 
71
                logging.warn('Error while registering embedded component: %s' % cmpName)
 
72
        else:
 
73
            logging.info('Skipping embedded %s registration, already installed.' % cmpName)
 
74
 
 
75
###############################################################################
 
76
 
 
77
class WarmupProgressHandler(NSObject):
 
78
    def init(self):
 
79
        self = super(WarmupProgressHandler, self).init()
 
80
        self.complete = False
 
81
        return self
 
82
    def loadStateChanged_(self, notification):
 
83
        self.handleLoadStateForMovie_(notification.object())
 
84
    def handleInitialLoadStateForMovie_(self, movie):
 
85
        load_state = movie.attributeForKey_(QTMovieLoadStateAttribute).longValue()
 
86
        if load_state < QTMovieLoadStateComplete:
 
87
            NSNotificationCenter.defaultCenter().addObserver_selector_name_object_(
 
88
                warmup_handler, 
 
89
                'loadStateChanged:', 
 
90
                QTMovieLoadStateDidChangeNotification, 
 
91
                warmup_movie)
 
92
        else:
 
93
            self.handleLoadStateForMovie_(movie)
 
94
    def handleLoadStateForMovie_(self, movie):
 
95
        load_state = movie.attributeForKey_(QTMovieLoadStateAttribute).longValue()
 
96
        if load_state == QTMovieLoadStateComplete:
 
97
            logging.info("QuickTime warm up complete")
 
98
            NSNotificationCenter.defaultCenter().removeObserver_(self)
 
99
            NSTimer.scheduledTimerWithTimeInterval_target_selector_userInfo_repeats_(
 
100
                10, self, 'releaseWarmupMovie:', None, False)
 
101
            self.complete = True
 
102
    def releaseWarmupMovie_(self, timer):
 
103
        logging.info("Releasing warmup movie.")
 
104
        global warmup_movie
 
105
        del warmup_movie
 
106
 
 
107
warmup_handler = WarmupProgressHandler.alloc().init()
 
108
warmup_movie = None
 
109
 
 
110
def warm_up():
 
111
    logging.info('Warming up QuickTime')
 
112
    rsrcPath = bundle.getBundleResourcePath()
 
113
 
 
114
    attributes = NSMutableDictionary.dictionary()
 
115
    attributes['QTMovieFileNameAttribute'] = os.path.join(rsrcPath, 'warmup.mov')
 
116
    attributes['QTMovieOpenAsyncRequiredAttribute'] = True
 
117
    attributes['QTMovieDelegateAttribute'] = None
 
118
 
 
119
    global warmup_movie
 
120
    warmup_movie, error = QTMovie.movieWithAttributes_error_(attributes, None)
 
121
    
 
122
    if error is not None:
 
123
        logging.warn("QuickTime Warm Up failed: %s" % error)
 
124
    else:
 
125
        warmup_handler.handleInitialLoadStateForMovie_(warmup_movie)
 
126
 
 
127
###############################################################################
 
128
 
 
129
def qttime2secs(qttime):
 
130
    timeScale = qttimescale(qttime)
 
131
    if timeScale == 0:
 
132
        return 0.0
 
133
    timeValue = qttimevalue(qttime)
 
134
    return timeValue / float(timeScale)
 
135
 
 
136
def qttimescale(qttime):
 
137
    if isinstance(qttime, tuple):
 
138
        return qttime[1]
 
139
    else:
 
140
        return qttime.timeScale
 
141
 
 
142
def qttimevalue(qttime):
 
143
    if isinstance(qttime, tuple):
 
144
        return qttime[0]
 
145
    else:
 
146
        return qttime.timeValue
 
147
 
 
148
###############################################################################
 
149
# The QTMediaTypeSubtitle and QTMediaTypeClosedCaption media types are only
 
150
# available in OS X 10.6 so we emulate them if they aren't defined.
 
151
 
 
152
try:
 
153
    dummy = QTMediaTypeSubtitle
 
154
except:
 
155
    QTMediaTypeSubtitle = u'sbtl'
 
156
 
 
157
try:
 
158
    dummy = QTMediaTypeClosedCaption
 
159
except:
 
160
    QTMediaTypeClosedCaption = u'clcp'
 
161
 
 
162
###############################################################################
 
163
 
 
164
class Player(player.Player):
 
165
 
 
166
    def __init__(self, supported_media_types):
 
167
        player.Player.__init__(self)
 
168
        self.supported_media_types = supported_media_types
 
169
        self.movie_notifications = None
 
170
        self.movie = None
 
171
        self.item_info = None
 
172
 
 
173
    def reset(self):
 
174
        threads.warn_if_not_on_main_thread('quicktime.Player.reset')
 
175
        if self.movie_notifications is not None:
 
176
            self.movie_notifications.disconnect()
 
177
        self.movie_notifications = None
 
178
        self.movie = None
 
179
        self.item_info = None
 
180
 
 
181
    def set_item(self, item_info, callback, errback, force_subtitles=False):
 
182
        threads.warn_if_not_on_main_thread('quicktime.Player.set_item')
 
183
        qtmovie = self.get_movie_from_file(item_info.video_path)
 
184
        self.reset()
 
185
        if qtmovie is not None:
 
186
            self.movie = qtmovie
 
187
            self.item_info = item_info
 
188
            self.movie_notifications = NotificationForwarder.create(self.movie)
 
189
            self.movie_notifications.connect(self.handle_movie_notification, QTMovieDidEndNotification)
 
190
            self.setup_subtitles(force_subtitles)
 
191
            callback()
 
192
        else:
 
193
            errback()
 
194
 
 
195
    def get_movie_from_file(self, path):
 
196
        osfilename = utils.filename_type_to_os_filename(path)
 
197
        url = NSURL.fileURLWithPath_(osfilename)
 
198
        qtmovie, error = QTMovie.movieWithURL_error_(url, None)
 
199
        if qtmovie is None or error is not None:
 
200
            return None
 
201
        if not self.can_open_file(qtmovie):
 
202
            return None
 
203
        return qtmovie
 
204
 
 
205
    def can_open_file(self, qtmovie):
 
206
        threads.warn_if_not_on_main_thread('quicktime.Player.can_open_file')
 
207
        can_open = False
 
208
        duration = utils.qttimevalue(qtmovie.duration())
 
209
        
 
210
        if qtmovie is not None and duration > 0:
 
211
            allTracks = qtmovie.tracks()
 
212
            if len(qtmovie.tracks()) > 0:
 
213
                # Make sure we have at least one track with a non zero length
 
214
                allMedia = [track.media() for track in allTracks]
 
215
                for media in allMedia:
 
216
                    mediaType = media.attributeForKey_(QTMediaTypeAttribute)
 
217
                    mediaDuration = utils.qttimevalue(media.attributeForKey_(QTMediaDurationAttribute).QTTimeValue())
 
218
                    if mediaType in self.supported_media_types and mediaDuration > 0:
 
219
                        can_open = True
 
220
                        break
 
221
 
 
222
        return can_open
 
223
 
 
224
    def setup_subtitles(self, force_subtitles):
 
225
        if config.get(prefs.ENABLE_SUBTITLES) or force_subtitles:
 
226
            enabled_tracks = self.get_all_enabled_subtitle_tracks()
 
227
            if len(enabled_tracks) == 0:
 
228
                tracks = self.get_subtitle_tracks()
 
229
                if len(tracks) > 0:
 
230
                    self.enable_subtitle_track(tracks[0])
 
231
            elif len(enabled_tracks) > 1:
 
232
                track_id = enabled_tracks[-1].attributeForKey_(QTTrackIDAttribute)
 
233
                self.disable_subtitles()
 
234
                self.enable_subtitle_track(track_id)
 
235
        else:
 
236
            self.disable_subtitles()
 
237
 
 
238
    def get_subtitle_tracks(self):
 
239
        tracks = list()
 
240
        if self.movie is not None:
 
241
            for track in self.movie.tracks():
 
242
                if self.is_subtitle_track(track):
 
243
                    media = track.media().quickTimeMedia()
 
244
                    lang = GetMediaLanguage(media)
 
245
                    display_name = track.attributeForKey_(QTTrackDisplayNameAttribute)
 
246
                    if lang == 32767:    # 32764 = langUndefined
 
247
                        name = display_name
 
248
                    else:
 
249
                        lang_code = script_codes.map_to_two_letters_code(lang)
 
250
                        lang_info = iso639.find(lang_code)
 
251
                        if lang_info is None:
 
252
                            name = display_name
 
253
                        else:
 
254
                            name = lang_info["name"]
 
255
                    if name != display_name:
 
256
                        name = "%s (%s)" % (name, display_name)
 
257
                    is_enabled = track.attributeForKey_(QTTrackEnabledAttribute) == 1
 
258
                    track_id = track.attributeForKey_(QTTrackIDAttribute)
 
259
                    tracks.append((track_id, name, is_enabled))
 
260
        return tracks
 
261
 
 
262
    def _find_track(self, key, value):
 
263
        if self.movie is not None:
 
264
            for track in self.movie.tracks():
 
265
                if self.is_subtitle_track(track):
 
266
                    if track.attributeForKey_(key) == value:
 
267
                        return track
 
268
        return None
 
269
 
 
270
    def _find_all_tracks(self, key, value):
 
271
        tracks = list()
 
272
        if self.movie is not None:
 
273
            for track in self.movie.tracks():
 
274
                if self.is_subtitle_track(track):
 
275
                    if track.attributeForKey_(key) == value:
 
276
                        tracks.append(track)
 
277
        return tracks
 
278
 
 
279
    def get_enabled_subtitle_track(self):
 
280
        return self._find_track(QTTrackEnabledAttribute, 1)
 
281
 
 
282
    def get_all_enabled_subtitle_tracks(self):
 
283
        return self._find_all_tracks(QTTrackEnabledAttribute, 1)
 
284
 
 
285
    def get_subtitle_track_by_name(self, name):
 
286
        return self._find_track(QTTrackDisplayNameAttribute, name)
 
287
 
 
288
    def get_subtitle_track_by_id(self, track_id):
 
289
        return self._find_track(QTTrackIDAttribute, track_id)
 
290
 
 
291
    def is_subtitle_track(self, track):
 
292
        layer = track.attributeForKey_(QTTrackLayerAttribute)
 
293
        media_type = track.attributeForKey_(QTTrackMediaTypeAttribute)
 
294
        return (layer == -1 and media_type == QTMediaTypeVideo) or media_type in [QTMediaTypeSubtitle, QTMediaTypeClosedCaption]
 
295
 
 
296
    def enable_subtitle_track(self, track_id):
 
297
        current = self.get_enabled_subtitle_track()
 
298
        if current is not None:
 
299
            current.setAttribute_forKey_(0, QTTrackEnabledAttribute)
 
300
        to_enable = self.get_subtitle_track_by_id(track_id)
 
301
        if to_enable is not None:
 
302
            to_enable.setAttribute_forKey_(1, QTTrackEnabledAttribute)
 
303
 
 
304
    def disable_subtitles(self):
 
305
        track = self.get_enabled_subtitle_track()
 
306
        if track is not None:
 
307
            track.setAttribute_forKey_(0, QTTrackEnabledAttribute)
 
308
 
 
309
    def select_subtitle_file(self, sub_path, handle_successful_select):
 
310
        def handle_ok():
 
311
            handle_successful_select()
 
312
        def handle_err():
 
313
            app.playback_manager.stop()
 
314
        copy_subtitle_file(sub_path, self.item_info.video_path)
 
315
        self.set_item(self.item_info, handle_ok, handle_err, True)
 
316
 
 
317
    def select_subtitle_encoding(self, encoding):
 
318
        # FIXME - set the subtitle encoding in quicktime
 
319
        pass
 
320
 
 
321
    def set_volume(self, volume):
 
322
        if self.movie:
 
323
            self.movie.setVolume_(volume)
 
324
 
 
325
    def get_elapsed_playback_time(self):
 
326
        qttime = self.movie.currentTime()
 
327
        return utils.qttime2secs(qttime)
 
328
 
 
329
    def get_total_playback_time(self):
 
330
        if self.movie is None:
 
331
            return 0
 
332
        qttime = self.movie.duration()
 
333
        return utils.qttime2secs(qttime)
 
334
 
 
335
    def skip_forward(self):
 
336
        current = self.get_elapsed_playback_time()
 
337
        duration = self.get_total_playback_time()
 
338
        pos = min(duration, current + 30.0)
 
339
        self.seek_to(pos / duration)
 
340
 
 
341
    def skip_backward(self):
 
342
        current = self.get_elapsed_playback_time()
 
343
        duration = self.get_total_playback_time()
 
344
        pos = max(0, current - 15.0)
 
345
        self.seek_to(pos / duration)
 
346
 
 
347
    def seek_to(self, position):
 
348
        qttime = self.movie.duration()
 
349
        if isinstance(qttime, tuple):
 
350
            qttime = (qttime[0] * position, qttime[1], qttime[2])
 
351
        else:
 
352
            qttime.timeValue = qttime.timeValue * position
 
353
        self.movie.setCurrentTime_(qttime)
 
354
 
 
355
    def play_from_time(self, resume_time=0):
 
356
        self.seek_to(resume_time / self.get_total_playback_time())
 
357
        self.play()
 
358
 
 
359
    def set_playback_rate(self, rate):
 
360
        self.movie.setRate_(rate)
 
361
 
 
362
    def handle_movie_notification(self, notification):
 
363
        if notification.name() == QTMovieDidEndNotification and not app.playback_manager.is_suspended:
 
364
            app.playback_manager.on_movie_finished()
 
365
 
 
366
###############################################################################