Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
avfmediaencoder.mm
Go to the documentation of this file.
1// Copyright (C) 2016 The Qt Company Ltd and/or its subsidiary(-ies).
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4
6
7#include <camera/avfcamera_p.h>
8#include <camera/avfcamerarenderer_p.h>
9#include <camera/avfcameraservice_p.h>
10#include <camera/avfcamerasession_p.h>
11#include <qdarwinformatsinfo_p.h>
12
13#include <QtMultimedia/qaudiodevice.h>
14#include <QtMultimedia/qmediadevices.h>
15#include <QtMultimedia/private/qavfcameradebug_p.h>
16#include <QtMultimedia/private/qavfcamerautility_p.h>
17#include <QtMultimedia/private/qcoreaudioutils_p.h>
18#include <QtMultimedia/private/qmediarecorder_p.h>
19#include <QtMultimedia/private/qmediastoragelocation_p.h>
20#include <QtMultimedia/private/qplatformaudioinput_p.h>
21#include <QtMultimedia/private/qplatformaudiooutput_p.h>
22#include <QtCore/qdebug.h>
23#include <QtCore/qmath.h>
24
25QT_USE_NAMESPACE
26
27namespace {
28
29bool qt_is_writable_file_URL(NSURL *fileURL)
30{
31 Q_ASSERT(fileURL);
32
33 if (![fileURL isFileURL])
34 return false;
35
36 if (NSString *path = [[fileURL path] stringByExpandingTildeInPath]) {
37 return [[NSFileManager defaultManager]
38 isWritableFileAtPath:[path stringByDeletingLastPathComponent]];
39 }
40
41 return false;
42}
43
44bool qt_file_exists(NSURL *fileURL)
45{
46 Q_ASSERT(fileURL);
47
48 if (NSString *path = [[fileURL path] stringByExpandingTildeInPath])
49 return [[NSFileManager defaultManager] fileExistsAtPath:path];
50
51 return false;
52}
53
54} // namespace
55
56AVFMediaEncoder::AVFMediaEncoder(QMediaRecorder *parent)
57 : QObject(parent)
58 , QPlatformMediaRecorder(parent)
59 , m_state(QMediaRecorder::StoppedState)
60 , m_duration(0)
61 , m_audioSettings(nil)
62 , m_videoSettings(nil)
63 //, m_restoreFPS(-1, -1)
64{
65 m_writer.reset([[QT_MANGLE_NAMESPACE(AVFMediaAssetWriter) alloc] initWithDelegate:this]);
66 if (!m_writer) {
67 qCDebug(qLcCamera) << Q_FUNC_INFO << "failed to create an asset writer";
68 return;
69 }
70}
71
73{
74 [m_writer abort];
75
76 if (m_audioSettings)
77 [m_audioSettings release];
78 if (m_videoSettings)
79 [m_videoSettings release];
80}
81
82bool AVFMediaEncoder::isLocationWritable(const QUrl &location) const
83{
84 return location.scheme() == QLatin1String("file") || location.scheme().isEmpty();
85}
86
88{
89 return m_state;
90}
91
93{
94 return m_duration;
95}
96
97void AVFMediaEncoder::updateDuration(qint64 duration)
98{
99 m_duration = duration;
100 durationChanged(m_duration);
101}
102
103static NSDictionary *avfAudioSettings(const QMediaEncoderSettings &encoderSettings, const QAudioFormat &format)
104{
105 NSMutableDictionary *settings = [NSMutableDictionary dictionary];
106
107 // Codec
108 int codecId = QDarwinFormatInfo::audioFormatForCodec(encoderSettings.mediaFormat().audioCodec());
109 settings[AVFormatIDKey] = @(codecId);
110
111 // Setting AVEncoderQualityKey is not allowed when format ID is alac or lpcm
112 if (codecId != kAudioFormatAppleLossless && codecId != kAudioFormatLinearPCM
113 && encoderSettings.encodingMode() == QMediaRecorder::ConstantQualityEncoding) {
114 // AudioQuality
115 int quality;
116 switch (encoderSettings.quality()) {
117 case QMediaRecorder::VeryLowQuality:
118 quality = AVAudioQualityMin;
119 break;
120 case QMediaRecorder::LowQuality:
121 quality = AVAudioQualityLow;
122 break;
123 case QMediaRecorder::HighQuality:
124 quality = AVAudioQualityHigh;
125 break;
126 case QMediaRecorder::VeryHighQuality:
127 quality = AVAudioQualityMax;
128 break;
129 case QMediaRecorder::NormalQuality:
130 default:
131 quality = AVAudioQualityMedium;
132 break;
133 }
134 settings[AVEncoderAudioQualityKey] = @(quality);
135 } else {
136 // BitRate
137 bool isBitRateSupported = false;
138 int bitRate = encoderSettings.audioBitRate();
139 if (bitRate > 0) {
140 QList<AudioValueRange> bitRates = qt_supported_bit_rates_for_format(codecId);
141 for (int i = 0; i < bitRates.count(); i++) {
142 if (bitRate >= bitRates[i].mMinimum &&
143 bitRate <= bitRates[i].mMaximum) {
144 isBitRateSupported = true;
145 break;
146 }
147 }
148 if (isBitRateSupported)
149 settings[AVEncoderBitRateKey] = @(encoderSettings.audioBitRate());
150 }
151 }
152
153 // SampleRate
154 int sampleRate = encoderSettings.audioSampleRate();
155 bool isSampleRateSupported = false;
156 if (sampleRate >= 8000 && sampleRate <= 192000) {
157 QList<AudioValueRange> sampleRates = qt_supported_sample_rates_for_format(codecId);
158 for (int i = 0; i < sampleRates.count(); i++) {
159 if (sampleRate >= sampleRates[i].mMinimum && sampleRate <= sampleRates[i].mMaximum) {
160 isSampleRateSupported = true;
161 break;
162 }
163 }
164 }
165 if (!isSampleRateSupported)
166 sampleRate = 44100;
167 settings[AVSampleRateKey] = @(sampleRate);
168
169 // Channels
170 int channelCount = encoderSettings.audioChannelCount();
171 bool isChannelCountSupported = false;
172 if (channelCount > 0) {
173 std::optional<QList<UInt32>> channelCounts = qt_supported_channel_counts_for_format(codecId);
174 // An std::nullopt result indicates that
175 // any number of channels can be encoded.
176 if (channelCounts == std::nullopt) {
177 isChannelCountSupported = true;
178 } else {
179 for (int i = 0; i < channelCounts.value().count(); i++) {
180 if ((UInt32)channelCount == channelCounts.value()[i]) {
181 isChannelCountSupported = true;
182 break;
183 }
184 }
185 }
186
187 // if channel count is provided and it's bigger than 2
188 // provide a supported channel layout
189 if (isChannelCountSupported && channelCount > 2) {
190 AudioChannelLayout channelLayout;
191 memset(&channelLayout, 0, sizeof(AudioChannelLayout));
192 auto channelLayoutTags = qt_supported_channel_layout_tags_for_format(codecId, channelCount);
193 if (channelLayoutTags.size()) {
194 channelLayout.mChannelLayoutTag = channelLayoutTags.first();
195 settings[AVChannelLayoutKey] = [NSData dataWithBytes:&channelLayout length:sizeof(channelLayout)];
196 } else {
197 isChannelCountSupported = false;
198 }
199 }
200
201 if (isChannelCountSupported)
202 settings[AVNumberOfChannelsKey] = @(channelCount);
203 }
204
205 if (!isChannelCountSupported) {
206 // fallback to providing channel layout if channel count is not specified or supported
207 UInt32 size = 0;
208 if (format.isValid()) {
209 auto layout = QCoreAudioUtils::toAudioChannelLayout(format, &size);
210 UInt32 layoutSize = offsetof(AudioChannelLayout, mChannelDescriptions)
211 + layout->mNumberChannelDescriptions * sizeof(AudioChannelDescription);
212 settings[AVChannelLayoutKey] = [NSData dataWithBytes:layout.get() length:layoutSize];
213 } else {
214 // finally default to setting channel count to 1
215 settings[AVNumberOfChannelsKey] = @(1);
216 }
217 }
218
219 if (codecId == kAudioFormatAppleLossless)
220 settings[AVEncoderBitDepthHintKey] = @(24);
221
222 if (codecId == kAudioFormatLinearPCM) {
223 settings[AVLinearPCMBitDepthKey] = @(16);
224 settings[AVLinearPCMIsBigEndianKey] = @NO;
225 settings[AVLinearPCMIsFloatKey] = @NO;
226 settings[AVLinearPCMIsNonInterleaved] = @NO;
227 }
228
229 return settings;
230}
231
232static NSDictionary *avfVideoSettings(QMediaEncoderSettings &encoderSettings,
233 AVCaptureDevice *device, AVCaptureConnection *connection,
234 QSize nativeSize)
235{
236 if (!device)
237 return nil;
238
239
240 // ### re-add needFpsChange
241// AVFPSRange currentFps = qt_current_framerates(device, connection);
242
243 NSMutableDictionary *videoSettings = [NSMutableDictionary dictionary];
244
245 // -- Codec
246
247 // AVVideoCodecKey is the only mandatory key
248 auto codec = encoderSettings.mediaFormat().videoCodec();
249 NSString *c = QDarwinFormatInfo::videoFormatForCodec(codec);
250 [videoSettings setObject:c forKey:AVVideoCodecKey];
251 [c release];
252
253 // -- Resolution
254
255 int w = encoderSettings.videoResolution().width();
256 int h = encoderSettings.videoResolution().height();
257
258 if (AVCaptureDeviceFormat *currentFormat = device.activeFormat) {
259 CMFormatDescriptionRef formatDesc = currentFormat.formatDescription;
260 CMVideoDimensions dim = CMVideoFormatDescriptionGetDimensions(formatDesc);
261 FourCharCode formatCodec = CMVideoFormatDescriptionGetCodecType(formatDesc);
262
263 // We have to change the device's activeFormat in 3 cases:
264 // - the requested recording resolution is higher than the current device resolution
265 // - the requested recording resolution has a different aspect ratio than the current device aspect ratio
266 // - the requested frame rate is not available for the current device format
267 AVCaptureDeviceFormat *newFormat = nil;
268 if ((w <= 0 || h <= 0)
269 && encoderSettings.videoFrameRate() > 0
270 && !qt_format_supports_framerate(currentFormat, encoderSettings.videoFrameRate())) {
271
272 newFormat = qt_find_best_framerate_match(device,
273 formatCodec,
274 encoderSettings.videoFrameRate());
275
276 } else if (w > 0 && h > 0) {
277 AVCaptureDeviceFormat *f = qt_find_best_resolution_match(device,
278 encoderSettings.videoResolution(),
279 formatCodec);
280
281 if (f) {
282 CMVideoDimensions d = CMVideoFormatDescriptionGetDimensions(f.formatDescription);
283 qreal fAspectRatio = qreal(d.width) / d.height;
284
285 if (w > dim.width || h > dim.height
286 || qAbs((qreal(dim.width) / dim.height) - fAspectRatio) > 0.01) {
287 newFormat = f;
288 }
289 }
290 }
291
292 if (qt_set_active_format(device, newFormat, false /*### !needFpsChange*/)) {
293 formatDesc = newFormat.formatDescription;
294 dim = CMVideoFormatDescriptionGetDimensions(formatDesc);
295 }
296
297 if (w < 0 || h < 0) {
298 w = dim.width;
299 h = dim.height;
300 }
301
302
303 if (w > 0 && h > 0) {
304 // Make sure the recording resolution has the same aspect ratio as the device's
305 // current resolution
306 qreal deviceAspectRatio = qreal(dim.width) / dim.height;
307 qreal recAspectRatio = qreal(w) / h;
308 if (qAbs(deviceAspectRatio - recAspectRatio) > 0.01) {
309 if (recAspectRatio > deviceAspectRatio)
310 w = qRound(h * deviceAspectRatio);
311 else
312 h = qRound(w / deviceAspectRatio);
313 }
314
315 // recording resolution can't be higher than the device's active resolution
316 w = qMin(w, dim.width);
317 h = qMin(h, dim.height);
318 }
319 }
320
321 if (w > 0 && h > 0) {
322 // Width and height must be divisible by 2
323 w += w & 1;
324 h += h & 1;
325
326 bool isPortrait = nativeSize.width() < nativeSize.height();
327 // Make sure the video has the right aspect ratio
328 if (isPortrait && h < w)
329 qSwap(w, h);
330 else if (!isPortrait && w < h)
331 qSwap(w, h);
332
333 encoderSettings.setVideoResolution(QSize(w, h));
334 } else {
335 w = nativeSize.width();
336 h = nativeSize.height();
337 encoderSettings.setVideoResolution(nativeSize);
338 }
339 videoSettings[AVVideoWidthKey] = @(w);
340 videoSettings[AVVideoHeightKey] = @(h);
341
342 // -- FPS
343
344 if (true /*needFpsChange*/) {
345 const qreal fps = encoderSettings.videoFrameRate();
346 qt_set_framerate_limits(device, connection, fps, fps);
347 }
348 encoderSettings.setVideoFrameRate(qt_current_framerates(device, connection).second);
349
350 // -- Codec Settings
351
352 NSMutableDictionary *codecProperties = [NSMutableDictionary dictionary];
353 int bitrate = -1;
354 float quality = -1.f;
355
356 if (encoderSettings.encodingMode() == QMediaRecorder::ConstantQualityEncoding) {
357 if (encoderSettings.quality() != QMediaRecorder::NormalQuality) {
358 if (codec != QMediaFormat::VideoCodec::MotionJPEG) {
359 qWarning("ConstantQualityEncoding is not supported for MotionJPEG");
360 } else {
361 switch (encoderSettings.quality()) {
362 case QMediaRecorder::VeryLowQuality:
363 quality = 0.f;
364 break;
365 case QMediaRecorder::LowQuality:
366 quality = 0.25f;
367 break;
368 case QMediaRecorder::HighQuality:
369 quality = 0.75f;
370 break;
371 case QMediaRecorder::VeryHighQuality:
372 quality = 1.f;
373 break;
374 default:
375 quality = -1.f; // NormalQuality, let the system decide
376 break;
377 }
378 }
379 }
380 } else if (encoderSettings.encodingMode() == QMediaRecorder::AverageBitRateEncoding){
381 if (codec != QMediaFormat::VideoCodec::H264 && codec != QMediaFormat::VideoCodec::H265)
382 qWarning() << "AverageBitRateEncoding is not supported for codec" << QMediaFormat::videoCodecName(codec);
383 else
384 bitrate = encoderSettings.videoBitRate();
385 } else {
386 qWarning("Encoding mode is not supported");
387 }
388
389 if (bitrate != -1)
390 codecProperties[AVVideoAverageBitRateKey] = @(bitrate);
391 if (quality != -1.f)
392 codecProperties[AVVideoQualityKey] = @(quality);
393
394 videoSettings[AVVideoCompressionPropertiesKey] = codecProperties;
395
396 return videoSettings;
397}
398
399void AVFMediaEncoder::applySettings(QMediaEncoderSettings &settings)
400{
401 unapplySettings();
402
403 AVFCameraSession *session = m_service->session();
404
405 // audio settings
406 const auto audioInput = m_service->audioInput();
407 const QAudioFormat audioFormat = audioInput ? audioInput->device.preferredFormat() : QAudioFormat();
408 m_audioSettings = avfAudioSettings(settings, audioFormat);
409 if (m_audioSettings)
410 [m_audioSettings retain];
411
412 // video settings
413 AVCaptureDevice *device = session->videoCaptureDevice();
414 if (!device)
415 return;
416 const AVFConfigurationLock lock(device); // prevents activeFormat from being overridden
417 AVCaptureConnection *conn = [session->videoOutput()->videoDataOutput() connectionWithMediaType:AVMediaTypeVideo];
418 auto nativeSize = session->videoOutput()->nativeSize();
419 m_videoSettings = avfVideoSettings(settings, device, conn, nativeSize);
420 if (m_videoSettings)
421 [m_videoSettings retain];
422}
423
424void AVFMediaEncoder::unapplySettings()
425{
426 if (m_audioSettings) {
427 [m_audioSettings release];
428 m_audioSettings = nil;
429 }
430 if (m_videoSettings) {
431 [m_videoSettings release];
432 m_videoSettings = nil;
433 }
434}
435
436void AVFMediaEncoder::setMetaData(const QMediaMetaData &metaData)
437{
438 m_metaData = metaData;
439}
440
442{
443 return m_metaData;
444}
445
446void AVFMediaEncoder::setCaptureSession(QPlatformMediaCaptureSession *session)
447{
448 AVFCameraService *captureSession = static_cast<AVFCameraService *>(session);
449 if (m_service == captureSession)
450 return;
451
452 if (m_service)
453 stop();
454
455 m_service = captureSession;
456 if (!m_service)
457 return;
458
459 connect(m_service, &AVFCameraService::cameraChanged, this, &AVFMediaEncoder::onCameraChanged);
460 onCameraChanged();
461}
462
463void AVFMediaEncoder::record(QMediaEncoderSettings &settings)
464{
465 if (!m_service || !m_service->session()) {
466 qWarning() << Q_FUNC_INFO << "Encoder is not set to a capture session";
467 return;
468 }
469
470 if (!m_writer) {
471 qCDebug(qLcCamera) << Q_FUNC_INFO << "Invalid recorder";
472 return;
473 }
474
475 if (QMediaRecorder::RecordingState == m_state)
476 return;
477
478 AVFCamera *cameraControl = m_service->avfCameraControl();
479 auto audioInput = m_service->audioInput();
480
481 if (!cameraControl && !audioInput) {
482 qWarning() << Q_FUNC_INFO << "Cannot record without any inputs";
483 updateError(QMediaRecorder::ResourceError, tr("No inputs specified"));
484 return;
485 }
486
487 // This is necessary to explicitly recreate m_audioInput inside AVFCameraSession.
488 // Which in turn is necessary for the case when the microphone was disconnected
489 // after stopping recording and reconnected.
491
492 m_service->session()->setActive(true);
493 const bool audioOnly = settings.videoCodec() == QMediaFormat::VideoCodec::Unspecified;
494 AVCaptureSession *session = m_service->session()->captureSession();
495 float rotation = 0;
496
497 if (!audioOnly) {
498 if (!cameraControl || !cameraControl->isActive()) {
499 qCDebug(qLcCamera) << Q_FUNC_INFO << "can not start record while camera is not active";
500 updateError(QMediaRecorder::ResourceError,
501 QMediaRecorderPrivate::msgFailedStartRecording());
502 return;
503 }
504 }
505
506 const QString path(outputLocation().scheme() == QLatin1String("file") ?
507 outputLocation().path() : outputLocation().toString());
508 const QUrl fileURL(QUrl::fromLocalFile(QMediaStorageLocation::generateFileName(
509 path, audioOnly ? QStandardPaths::MusicLocation : QStandardPaths::MoviesLocation,
510 settings.preferredSuffix())));
511
512 NSURL *nsFileURL = fileURL.toNSURL();
513 if (!nsFileURL) {
514 qWarning() << Q_FUNC_INFO << "invalid output URL:" << fileURL;
515 updateError(QMediaRecorder::ResourceError, tr("Invalid output file URL"));
516 return;
517 }
518 if (!qt_is_writable_file_URL(nsFileURL)) {
519 qWarning() << Q_FUNC_INFO << "invalid output URL:" << fileURL
520 << "(the location is not writable)";
521 updateError(QMediaRecorder::ResourceError, tr("Non-writeable file location"));
522 return;
523 }
524 if (qt_file_exists(nsFileURL)) {
525 // We test for/handle this error here since AWAssetWriter will raise an
526 // Objective-C exception, which is not good at all.
527 qWarning() << Q_FUNC_INFO << "invalid output URL:" << fileURL
528 << "(file already exists)";
529 updateError(QMediaRecorder::ResourceError, tr("File already exists"));
530 return;
531 }
532
533 applySettings(settings);
534
535 QVideoOutputOrientationHandler::setIsRecording(true);
536
537 // We stop session now so that no more frames for renderer's queue
538 // generated, will restart in assetWriterStarted.
539 [session stopRunning];
540
541 if ([m_writer setupWithFileURL:nsFileURL
542 cameraService:m_service
543 audioSettings:m_audioSettings
544 videoSettings:m_videoSettings
545 fileFormat:settings.fileFormat()
546 transform:CGAffineTransformMakeRotation(qDegreesToRadians(rotation))]) {
547
548 m_state = QMediaRecorder::RecordingState;
549
550 actualLocationChanged(fileURL);
551 stateChanged(m_state);
552
553 // Apple recommends to call startRunning and do all
554 // setup on a special queue, and that's what we had
555 // initially (dispatch_async to writerQueue). Unfortunately,
556 // writer's queue is not the only queue/thread that can
557 // access/modify the session, and as a result we have
558 // all possible data/race-conditions with Obj-C exceptions
559 // at best and something worse in general.
560 // Now we try to only modify session on the same thread.
561 [m_writer start];
562 } else {
563 [session startRunning];
564 updateError(QMediaRecorder::FormatError, QMediaRecorderPrivate::msgFailedStartRecording());
565 }
566}
567
569{
570 if (!m_service || !m_service->session() || state() != QMediaRecorder::RecordingState)
571 return;
572
573 toggleRecord(false);
574 m_state = QMediaRecorder::PausedState;
575 stateChanged(m_state);
576}
577
579{
580 if (!m_service || !m_service->session() || state() != QMediaRecorder::PausedState)
581 return;
582
583 toggleRecord(true);
584 m_state = QMediaRecorder::RecordingState;
585 stateChanged(m_state);
586}
587
589{
590 if (m_state != QMediaRecorder::StoppedState) {
591 // Do not check the camera status, we can stop if we started.
592 stopWriter();
593 }
594 QVideoOutputOrientationHandler::setIsRecording(false);
595}
596
597
599{
600 if (!m_service || !m_service->session())
601 return;
602
603 if (!enable)
604 [m_writer pause];
605 else
606 [m_writer resume];
607}
608
609void AVFMediaEncoder::assetWriterStarted()
610{
611}
612
613void AVFMediaEncoder::assetWriterFinished()
614{
615
616 const QMediaRecorder::RecorderState lastState = m_state;
617
618 unapplySettings();
619
620 if (m_service) {
621 AVFCameraSession *session = m_service->session();
622
623 if (session->videoOutput()) {
625 }
626 if (session->audioPreviewDelegate()) {
627 [session->audioPreviewDelegate() resetAudioPreviewDelegate];
628 }
629 if (session->videoOutput() || session->audioPreviewDelegate())
630 [session->captureSession() startRunning];
631 }
632
633 m_state = QMediaRecorder::StoppedState;
634 if (m_state != lastState)
635 stateChanged(m_state);
636}
637
638void AVFMediaEncoder::assetWriterError(QString err)
639{
640 updateError(QMediaRecorder::FormatError, err);
641 if (m_state != QMediaRecorder::StoppedState)
642 stopWriter();
643}
644
645void AVFMediaEncoder::onCameraChanged()
646{
647 if (m_service && m_service->avfCameraControl()) {
648 AVFCamera *cameraControl = m_service->avfCameraControl();
649 connect(cameraControl, SIGNAL(activeChanged(bool)),
650 SLOT(cameraActiveChanged(bool)));
651 }
652}
653
654void AVFMediaEncoder::cameraActiveChanged(bool active)
655{
656 Q_ASSERT(m_service);
657 AVFCamera *cameraControl = m_service->avfCameraControl();
658 Q_ASSERT(cameraControl);
659
660 if (!active) {
661 return stopWriter();
662 }
663}
664
665void AVFMediaEncoder::stopWriter()
666{
667 [m_writer stop];
668}
669
670#include "moc_avfmediaencoder_p.cpp"
static NSDictionary * avfAudioSettings(const QMediaEncoderSettings &encoderSettings, const QAudioFormat &format)
static NSDictionary * avfVideoSettings(QMediaEncoderSettings &encoderSettings, AVCaptureDevice *device, AVCaptureConnection *connection, QSize nativeSize)
void resetCaptureDelegate() const
AVFCamera * avfCameraControl() const
AVFCameraSession * session() const
AVFCameraRenderer * videoOutput() const
QMediaMetaData metaData() const override
void setMetaData(const QMediaMetaData &) override
void updateDuration(qint64 duration)
qint64 duration() const override
void stop() override
void resume() override
void toggleRecord(bool enable)
bool isLocationWritable(const QUrl &location) const override
void record(QMediaEncoderSettings &settings) override
~AVFMediaEncoder() override
QMediaRecorder::RecorderState state() const override
void pause() override
void setCaptureSession(QPlatformMediaCaptureSession *session)
bool qt_is_writable_file_URL(NSURL *fileURL)
bool qt_file_exists(NSURL *fileURL)