- كل المقالات
- ربط libVLC مباشرةً من Swift 6
ربط libVLC مباشرةً من Swift 6
احتاج تطبيق IPTV إلى «صورة داخل صورة»، وخطُّ VLCKit الذي كان يقوم عليه لا يملكها. ملاحظات في امتلاك محرّك C بدل استهلاكه — عزلُ المؤشّرات، وأعمارٌ لا يفحصها شيء، ومجموعةُ رقعٍ مرتّبة على شيفرة VLC نفسها، وما كلّفه ذلك.
«تلفاز» تطبيق IPTV، تجده في متجر التطبيقات، ويعمل على iPhone وiPad وMac وApple TV. وIPTV تعني ما يقرّر المزوّد أن يرسله إليك: MPEG-TS فوق UDP، وبياناتِ HLS تتقادم بين مقطعٍ وآخر، وحاوياتِ MKV، وترجماتِ SSA تحمل تنسيقها معها، وصوتًا بترميزاتٍ لم توفّر آبل لها فاكَّ ترميزٍ قطّ.
وإطار AVFoundation ممتاز في الصيغ التي تدعمها آبل. أمّا القول إنّ «AVFoundation لا يصلح لـ IPTV» فدعوى يصحّحها لك خلال ساعةٍ واحدة رجلٌ أصدر تطبيق HLS لا غبار عليه، ولم يجد يومًا سببًا لأن يُطعمه شيئًا سواه. وإليك المسألة في أصغر صورها.
خُذ ملفَّ MP4 يحمل فيديو H.264 وصوت AAC، وأعد تغليفه إلى Matroska بـ -c copy.
تخرج المسارات الأوّليّة متطابقةً بايتًا ببايت — وقد تحقّقتُ من بصماتها. يُفَكّ
ترميز MP4، أمّا MKV فيردّ -11828، صيغة الوسيط هذه غير مدعومة. إذ لا فاكَّ
تغليفٍ لـ Matroska أصلًا، فلا يُسأل البتّة عمّا في الداخل.
- فُكّ ترميزها
- تُفتح ومسارات ناقصة
- مرفوضة — −11828
| الملفّ | macOS 26 | iOS 26 |
|---|---|---|
| base.mp4H.264 · AAC | ||
| h264_aac.mkvsame streams, remuxed | ||
| vp9_opus.webmVP9 · Opus | ||
| h264_aac.flvH.264 · AAC | ||
| h264_ac3.mp4Dolby Digital | ||
| h264_aac.tsH.264 · AAC | ||
| h264_mp2.tsMPEG-1 Layer II | ||
| hevc_aac.tsHEVC · AAC | no video | |
| h264_dts.tsH.264 · DTS | no audio | |
| h264_opus.tsH.264 · Opus | no audio |
كلّ نجاحٍ ناقصٍ يُبلغ أنّ isPlayable == true ولا يرفع خطأ. وHEVC داخل تيّار نقلٍ أمرٌ مألوف في IPTV.
والرفض هو النصف الرخيص: العطب الصريح عطبٌ تستطيع التعامل معه. أمّا الصفوف
الثلاثة الأخيرة فهي الباهظة. يُفتح HEVC داخل تيّار نقلٍ على macOS، ويُبلغ أنّ
isPlayable == true، ولا يرفع خطأً، ثمّ يناولك أصلًا بلا مسار فيديو — وHEVC
داخل تيّار نقلٍ أمرٌ مألوفٌ في IPTV. فلا يرى المشاهد رسالة «صيغة غير مدعومة»، بل
يرى قناةً تبثّ صوتًا فوق مستطيلٍ أسود، فيخبرك أنّ تطبيقك معطوب، وهو محقّ.
وبعد ذلك بزمنٍ طويل، وأنا أبني تطبيقات العرض للمكتبة التي خرجت من هذا كلّه، احتجتُ ملفًّا واحدًا يمتحن كلّ ما ينبغي لمشغّلٍ أن يحتمله. فكان ستّين ثانيةً من Matroska تحمل ثلاث نسخٍ للفيديو، وثلاثة مسارات صوت Opus، وثلاثة مسارات ترجمة — أحدها بالعربيّة من اليمين إلى اليسار — وستّة فصولٍ مسمّاة، وصورةَ غلافٍ مرفقة. وليست خاصّيةٌ واحدة من هذه في متناول AVFoundation. بنيتُه عرضًا توضيحيًّا، وهو في حقيقته الفجوةُ نفسها مكتوبةً في ملفّ.
كنتُ أستعمل VLCKit أصلًا
فكان لا بدّ أن يكون المحرّك libVLC — نواةَ التشغيل لدى VideoLAN، وهي واجهة C نفسها التي يقوم عليها VLC. والغلاف المعروف لمنصّات آبل هو VLCKit، وهو أيضًا من صنع VideoLAN، مكتوبٌ بـ Objective-C. وكنتُ أستعمله أصلًا.
وVLCKit أثرٌ أمينٌ للعقد الذي كُتب فيه، وأقولها على وجهها: بروتوكولُ المفوَّض،
وأسماءُ الإشعارات، وid drawable الذي تُسنِد إليه عرضًا — هكذا كانت تُبنى أُطر
الوسائط في 2013، وكثيرٌ ممّا تعتمد عليه اليوم بُني على المنوال نفسه ويعمل.
ولم أغادر VLCKit بسبب الأسلوب، بل بسبب «صورة داخل صورة»، والسببُ أهونُ من أن
يُعدّ حجّةً في التصميم: خطُّ VLCKit 3 لا يملكها. لا نوعَ لها، ولا بروتوكول، ولا
وجودَ لـ VLCDrawable.h في ذلك الخطّ أصلًا — فالميزة إنّما تصل في VLCKit 4،
وVLCKit 4 في طور ألفا منذ أيلول 2022، وتُوزَّع من مسار تنزيلٍ فيه كلمة
unstable. فلم يكن الخيار إذًا بين Swift حديثة وObjective-C تفي بالغرض، بل كان:
إمّا أن أعتمد في الإنتاج على نسخةٍ اختباريّةٍ عمرها أربع سنوات لأنال ميزةً
واحدة، وإمّا أن أربط واجهة C التي تحتها بنفسي. وكلُّ ما تلا ذلك القرار — الربطُ
المباشر بـ C، وقواعدُ الأعمار، وأكثرُ الرقع الواردة في هذه المقالة — منه ينبع.
حيث يتقدّم الغلاف
- مسار الاستدعاء
- Swift 6، ورَبْطٌ مباشر بلغة C دون Objective-C في الوسط
- Objective-C من أوّلها إلى آخرها — ولا سطر Swift واحد في مصادرها
- التزامن
- المشغّل على المُمثِّل الرئيسيّ وقابلٌ للرصد، وأنواع الوسائط Sendable، والتحميل يأخذ قيمةً مُرسَلة
- لا دعم لتزامن Swift. والأصل أن يصل نداء المفوَّض متزامنًا على خيط libVLC نفسه، والقفز إلى الرئيسيّ شأنك أنت
- الأحداث
- تدفّق AsyncStream لكلّ مشترك، مستقلّ، وسياسة تخزين لكلّ مسار
- مفوَّضٌ واحد ومركز إشعارات، وخمسٌ من نداءاتها ما تزال تناولك NSNotification
- الأخطاء
- أخطاءٌ مصنّفة، تسع حالات، تحمل كلٌّ منها سياقها
- لا نوع خطأ البتّة — إرجاع فارغ، وقيم منطقيّة، ونصٌّ واحد يخصّ الخيط ويصلح حتى النداء التالي
- SwiftUI
- تأتي معها واجهة عرض جاهزة
- أسنِد سطح الرسم واكتب غلافك لـ SwiftUI بنفسك
حيث تتقدّم VLCKit
- الربط
- أرشيفٌ ساكن في كلّ شريحة
- إطارٌ ديناميكيّ — وأُسقط الربط الساكن عمدًا في الإصدار 4
- مسار إعادة الربط في LGPL
- الربط الساكن لا يترك للمستهلك مسارًا عمليًّا، والمسألة المفتوحة التي تطلبه بلا جواب
- الربط الديناميكيّ هو الجواب المتعارف عليه، وهو ما توزّعه VideoLAN نفسها
- تقارير الأعطال
- مجرّدةٌ من رموز التنقيح، فالعطل داخل libVLC لا تُفَكّ رموزه
- رموز التنقيح تأتي داخل الإطار
- المدى
- من iOS 18 فصاعدًا، ومعها Mac Catalyst التي لا شريحة لها في VLCKit
- رجوعًا إلى iOS 12 وmacOS 10.13، ومعها watchOS — أي نحو ستّ سنوات إضافيّة من الأجهزة
- المحرّك
- libVLC 4 وحده، وهو لم يصدر إصدارًا مستقرًّا قطّ
- خطّان: واحدٌ على libVLC 3 الصادر وما يزال يُصلَح، وآخر على 4
- السجلّ
- تطبيقٌ واحد
- VLC على iOS وiPadOS وtvOS — وقد انتقل إلى هذه النسخة الاختباريّة بعينها عبر SPM في تموز
- القائمون عليها
- شخصٌ واحد
- VideoLAN وVideolabs، على مصادر تعود إلى أوّل استيرادٍ للإطار سنة 2007
لكلٍّ منهما مجموعة رقعٍ خاصّة به على VLC — سبعٌ وعشرون لـ VLCKit، وخمسٌ وعشرون لـ SwiftVLC. ورقع المحرّك هي الكلفة المعتادة لإصدار libVLC على منصّات آبل، لا ميزةٌ تميّز أيًّا منهما.
وأمران لولاهما لكان ذلك الجدول غير أمين. أوّلهما أنّ VLCKit 4 لم تقف مكانها: أضافت visionOS وwatchOS، وأعادت كتابة إدارة الأحداث، ونالت تنقّلًا زمنيًّا دون الثانية، وحازت دعم Swift Package Manager قبل أسبوعين من كتابتي هذه السطور. والمقارنةُ بملفّ README الخاصّ بالخطّ الثالث — وهو ما يعرضه مستودعها عليك إلى اليوم — مقارنةٌ بخصمٍ من قشّ.
وثانيهما أنّ المحرّك غير الصادر محرّكي أنا لا محرّكهم: فأنا على libVLC 4، ولم يصدر منه إصدارٌ مستقرٌّ قطّ، بينما تُبقي VLCKit خطًّا على libVLC 3 الصادر وما تزال تُصلح فيه. فإن كنتَ تحتاج محرّكًا قد صدر فعلًا، ففي هذه المقارنة خيارٌ واحدٌ بالضبط، وليس خياري.
وثمّة بلاغٌ مفتوحٌ موسومٌ بـ Blocker على متتبّع VideoLAN، سجّله في تموز غيري، يصف مدخلًا ثانيًا على مشغّلٍ مُعادِ الاستعمال لا يعيد ربط خرج الفيديو أبدًا: صوتٌ يعمل خلف صورةٍ سوداء دائمة، على tvOS، على MPEG-TS حيٍّ من لوحة IPTV. وهذا تبديلُ قنوات. وهذا هو الحِمل بعينه.
المحاولة التي سبقت المحاولة
في كانون الأول 2025 نشرتُ harflabs/VLC.
أربعةُ إيداعاتٍ في مساءٍ واحد، ثم لا شيء بعدها أبدًا.
ولم تكن حزمةَ libVLC. أخذت الأُطر الثلاثة المكتوبة بـ Objective-C التي توفّرها
VLCKit 3.7.0، إطارًا لكلّ منصّة، وأعادت نشرها أهدافًا ثنائيّةً في SPM خلف
مظلّةٍ مشروطةٍ بالمنصّة — إذ لا Package.swift لخطّ VLCKit 3 أصلًا، وأردتُ أن
أكتب عنوانًا في بيان الحزمة كما يفعل سائر الناس.
وهي مؤرشفةٌ اليوم، والسببُ ظاهرٌ في بيانها نفسه: يُعلن swift-tools-version: 6.0 ثم يغلّف إطارًا لا تحمل ترويساته العامّة سمةَ تزامنٍ واحدة. وتبيّن أنّ
التحزيم هو النصف السهل: جعلتُ VLCKit قابلةً للاستحضار، ولم أغيّر شيئًا في أنّ
تطبيق SwiftUI بـ Swift 6 صار يملك نموذجَ كائناتٍ بلا سماتِ تزامن، بمفوَّضٍ واحد.
وبدأ SwiftVLC في شباط، وبلغ 1.0 في تموز. وبقيّةُ هذه المقالة تُبيّن أين ذهبت تلك الأشهر الأربعة والنصف.
اربط C مباشرةً، واقبل الفاتورة
كان القرار الأول أن أنتقل من C إلى Swift بلا Objective-C في المنتصف.
وقد ساق الجدولُ أعلاه تلك الحجّة، وما لا يستطيع أن يُظهره أنّ الرسم كلَّه يصير عرضًا واحدًا:
struct PlayerView: View {
@State private var player = Player()
var body: some View {
VideoView(player)
.onAppear { try? player.play(url: streamURL) }
}
}
يسلّم VideoView إلى libVLC كائن NSView/UIView عبر set_nsobject فيرسم
VLC فيه مباشرة. لا طبقةَ تبنيها أنت، ولا MTKView، ولا AVPlayerLayer.
أمّا الفاتورةُ فهي أنّك صرتَ تملك كلَّ قواعد أعمار C بنفسك، بلا طبقةٍ على هيئة
ARC تختبئ خلفها. وكلُّ كائنٍ في libVLC يتبع النمط ذاته — init يخصّص، وdeinit
يحرّر، وعمرُ كائن Swift يملك عمرَ مؤشّر C — وهذا الجزء آليّ. أمّا الإخفاقات
المثيرة فتقع كلّها عند المفاصل.
طريقتان لنقل مؤشّر
لا يطابق OpaquePointer ولا UnsafeMutableRawPointer بروتوكول Sendable. وليس
ذلك سهوًا، ولا نتيجةً للعزل المناطقيّ: بل تُعلن المكتبة القياسيّة هذه المطابقة
غير متاحة، عن قصد، لأنّ المترجم لا يستطيع أن يستدلّ على ما يشير إليه المؤشّر.
والمناطق إنّما تُنقذك أحيانًا بأن تسمح بنقل قيمةٍ منفصلة بين نطاقات العزل، وهي
لا تنفع هنا، لأنّ الالتقاط داخل مُغلَّفٍ هاربٍ موسومٍ بـ @Sendable ليس نقلًا،
وتحريرُ كائن C خارج الخيط الرئيسي هو ذلك الالتقاط بعينه.
ويسمح SwiftVLC بمخرجين اثنين. فللالتقاط داخل مُغلَّفٍ واحد، ربطٌ محليّ ينسحب من الفحص:
nonisolated(unsafe) let p = pointer
DispatchQueue.global(qos: .utility).async {
libvlc_media_player_release(p)
}
فالمؤشّر قابلٌ للنقل بداهةً ويبقى صالحًا طوال النطاق المحيط، وهذه هي الحجّة
كلّها. أمّا المؤشّرات التي تُقرأ وتُكتب من خيوطٍ عدّة عبر الزمن، فـMutex حول
بنية حالة، والبنية موسومةٌ بـ @unchecked Sendable، والقفلُ — لا المترجم — هو
من يؤدّي العمل الفعليّ.
وثمّة طريقةٌ ثالثة تحظرها وثيقةُ البنية: تهريبُ المؤشّر عبر Int(bitPattern:)
والعودةُ به. إنّها تُترجَم، وتُسكِت التحذير، وتُتلف الشيئين اللذين جعلا التحذير
جديرًا بالوجود: النوعَ والقصد. وبعد ستّة أشهر لن يستطيع قارئُ تلك الدالّة أن
يميّز مؤشّرًا حيًّا من عددٍ صحيح، ويكون المترجم قد أُقنع بالكفّ عن المساعدة.
وتخرق الشيفرةُ قاعدتَها هذه في موضعٍ واحدٍ بالضبط: مسارٌ للتأهيل يخزّن عنوان
المشغّل بوصفه UInt ثمّ يعيد تحويله إلى مؤشّرٍ مباشرةً، وتعليقُ الحقل نفسه يسوق
العذر المحظور — مخزَّنٌ بتّاتٍ لأنّ OpaquePointer ليس Sendable. وقد تركتُه
على حاله. فالقاعدةُ ذاتُ الاستثناء الواحد الذي تستطيع الإشارة إليه، والقاعدةُ
التي كفّ الناس عن إنفاذها، تتشابهان تمامًا من الخارج، ولهذا وحده وُجدت هذه
الفقرة.
أعمارٌ لا يفحصها شيء
تعبر الأحداثُ ثلاثةَ نطاقات عزل: نداءاتُ C التي تنطلق على خيوط libVLC نفسها، ثم
مذيعٌ متعدّد المستهلكين، ثم خصائصُ @Observable على المُمثِّل الرئيسي.
تلتقط Broadcaster.broadcast المشتركين تحت القفل، ثم تشغّل مرشّحاتهم وتسلّم
القيمة خارجه. سطرٌ واحد.
ويسوء أمران منفصلان إن سلّمتَ من الداخل. أوّلهما أنّ مرشّح المشترك شيفرةُ
مستخدم، فإن مسّت تلك الشيفرةُ المذيعَ من جديد — باشتراكٍ، أو بسؤالٍ عن خلوّه، أو
ببثّ — فوقعت في استعصاءٍ مع قفلٍ لا يقبل إعادة الحيازة؛ وهي ما تنتظرُ لا تزال
حائزةً قفلًا يقف عليه خيطُ أحداث libVLC نفسه. وثانيهما أنّ onTermination تعمل
في الحال على الخيط الذي ألغى المهمّة المستهلِكة، وتستدعي unsubscribe التي تطلب
ذلك القفل بعينه. فإلغاءٌ يقع في وسط البثّ يصير خيطًا واحدًا يطلب قفلًا هو حائزُه
أصلًا.
ولهذا اختبارُ انحدارٍ اليوم، وُجد لأنّ العلاج سطرٌ واحد وقد يردّه أحدهم إلى داخل القفل. ولم يكن له اختبارٌ يوم كُتبت العلّة. فالاستعصاءات على هذه الشاكلة لا تظهر في حزمة اختباراتٍ تنتهي وأنت ما تزال تنظر إليها، وإنّما تظهر بين يدي من يسحب شريطَ بثٍّ حيٍّ في القطار.
ويحمل النوعُ نفسه طريقتين للإغلاق: تُغلق finishAll() المشتركين الحاليين وتسمح
بالاشتراك من جديد، أمّا terminate() فتغلقهم وتجعل كلَّ استدعاء subscribe
لاحق يعيد تدفّقًا منتهيًا فورًا. ووُجدت الثانية لأنّ بعض المذيعين يُبلَغ إليهم
عبر خاصّيةٍ محسوبة تبني اشتراكًا جديدًا مع كلّ وصول، فوجب أن ينال المشتركُ الذي
يصل بعد زوال المنتِج تدفّقًا منتهيًا، لا تدفّقًا حيًّا لن يُطعمه أحدٌ أبدًا.
وبدونها لا يكون العطب انهيارًا، بل await لا يعود أبدًا، وهذا أعسر في التشخيص
بكثير.
كذلك وجب أن يصير النداءُ الواحد تدفّقين، لأنّ libVLC يجمع فيه بين نوعين من الأحداث، ولا يجوز أن يُسقَط بأمانٍ إلا أحدُهما.
نداء C واحد، على خيط libVLC
التحكّم24 حالة
هويّة الوسيط، وقدراته، ودورة حياته، ونهاياته.
كلٌّ منها يخبر بأمرٍ وقع مرّة واحدة، ولا يعيده حدثٌ لاحق، فسقوط واحدٍ منها ضياعُ خبر.
بلا حدّتنمو الذاكرة بمقدار تأخّر المستهلك في معدّل التحكّم، لا في معدّل الساعة.
التوقيت4 حالات
ساعة التشغيل، وامتلاء المخزن، وعدّادات العرض.
كلٌّ منها يَنسخ ما قبله، فإسقاط المتراكم يكلّف دقّةً ولا يكلّف غيرها.
أحدث 4المستهلك المتأخّر يتخطّى العيّنات البائتة ويظلّ يتلقّى الأحدث.
حدث timeChanged وحده ينطلق نحو ثلاثين مرّة في الثانية. وعبر مخزنٍ محدود واحد، يفقد المستهلكُ المتوقّف ثانيتين ما اصطفّ خلفه — تغيُّرَ وسيطٍ، أو بلوغَ نهاية.
الحدّ الذي لا يحلّه
«أحدث أربعة» محسوبةٌ على مسار التوقيت كلّه لا خانةً لكلّ نوع، فدفقةٌ طويلة من أسرع الأنواع قد تُزيح أحدثَ عيّنةٍ من نوعٍ أبطأ. أمّا ما يضمنه الفصل فهو النصف الذي يَعنينا: لا يمكن لما يسقط أن يكون حدث تحكّمٍ يقع مرّة واحدة، لأنّ أحداث التحكّم ليست في ذلك المخزن أصلًا.
ويبدو الهدم، إذا وُصف بالكلام، كأنّه قائمةٌ من أربعة أشياء بترتيبٍ ثابت. وليس بقائمة، بل هو مجموعةُ قيودٍ يصادف أنّ ذلك الترتيب يفي بها، وأنفعُ طريقةٍ لإمساكه أن تسأل: ما الذي لا يزال يقرأ هذا؟
والقيود الثلاثة كلّها تعيش في تعليقٍ واحدٍ فوق دالّةٍ واحدة، وهو وحده ما يُنفِذها.
المؤشّر ليس هويّة
هذه هي الفكرة التي كنتُ أودّ لو أُخبرتُ بها قبل أن أبدأ، وهي التي بلغتُها أخيرًا.
يستطيع Player أن يستبدل مشغّل libVLC القابع تحته بينما يبقى كائن Swift
وتدفّقات أحداثه على قيد الحياة. وحين يقع ذلك تصل النداءاتُ الجاريةُ لا محالة،
وستصل واصفةً شيئًا لم يعد موجودًا. والبديهةُ أن تقارن المؤشّرات، والبديهةُ
خاطئة، والمصدر يقول السبب في سطر: عناوينُ المؤشّرات الأصليّة ليست هويّات — فقد
يعيد المُخصِّص عنوانًا متقاعدًا لمقبضٍ لاحق.
ولذلك تنال كلُّ طبقةٍ يمكن أن تُستبدل تحت مستهلكٍ حيّ عدّادًا تصاعديًّا، ويحمل كلُّ حدثٍ القيمَ التي وُلد بها.
ولم أصمّم هذا سلفًا، بل تراكم صنفَ عللٍ بعد صنف؛ ولمّا دقّقتُ المكتبة بعد 1.0 كانت أكبرُ مجموعةٍ من الملاحظات صورةً من صور هذا النداء يتحدّث عن الشيء السابق. ولو ابتدأتُ من جديد لمددتُ يدي إليه في اليوم الأول، وأحسب ذلك صحيحًا في حقّ كلّ من يربط مكتبة C تعمّر كائناتُها بعد النداءات التي أنشأتها.
حيث يكفّ الغلاف عن كونه غلافًا
والحقيقةُ الحاكمة أنّ libVLC ينسخ مؤشّرات نداءات ذاكرة الفيديو لديك وسياقَها المبهم حين يُفتح خرجُ فيديو. ومسحُ متغيّرات النداء على مشغّل الوسائط بعد ذلك لا يُبطل نسخةً يحوزها ذلك الخرج أصلًا. فالهدم البديهيّ — امسح النداءات ثمّ حرّر السياق — استخدامٌ بعد التحرير ينتظر خرجًا لم يفرغ منك بعد.
والعلاج يربط كلَّ سياقٍ محتجَز بمؤشّر libvlc_media_player_t واحدٍ بعينه،
فتستطيع المتحكّمات المتعاقبة على المقبض الأصليّ نفسه أن تتسلّم ذرّيًّا، بينما لا
يستطيع خرجٌ متداخل أن يمسّ أبعادَ خرجٍ آخر ولا مجمّعَه ولا حالةَ تنظيفه. وإحالةُ
السياق إلى التقاعد توقف عملَ العرض الجديد فورًا، لكنّه لا يُحرَّر إلا بعد أن
يعود آخرُ تحريرٍ أصليٍّ محسوبٍ لذلك المقبض وتنضب كلُّ النداءات الجارية. ولا
تُعامَل مهلةٌ زمنيّة ولا ملاحظةٌ عابرةٌ لخرج الفيديو دليلًا على الأمان. فمع
نداءات C، عبارةُ «الغالب أنّه انتهى الآن» ليست عمرًا.
ثمّ إنّ مسارات العرض أربعة، وأظهرُ ما تفترق فيه هو مَن يملك الكائن في نهايتها.
VideoViewكل المنصّات
set_nsobjectNSView / UIView
VLC يرسم داخل عرضك
PiPVideoViewiOS
وسيط الرسمخرج عازل العيّنات في VLCAVPictureInPictureController
المتحكّم يملكه libVLC
PiPControllerمباشر، واجهة عامّة
نداءات vmemCVPixelBufferCMSampleBufferAVSampleBufferDisplayLayer
الطبقة تملكها SwiftVLC · BGRA ثمانيّ، نطاق قياسي
PiPVideoViewmacOS، بتفعيل صريح
set_nsobjectNSView الخاص بـ VLCPIPViewController
إطار PIP.framework الخاص · معطّل افتراضيًا
وذلك الصفُّ الأخير هو القرار الذي أنا أقلُّ ارتياحًا له وأكثرُ ثقةً بصوابه. فعلى
macOS تقتطع مرآةُ عازل العيّنات العامّة عند حجم طبقةٍ 1:1 بدل أن تتحجّم داخل
لوحة «صورة داخل صورة»، في إصدارات macOS التي أدعمها — أي أنّ الصورة ببساطة خطأٌ
على الشاشة. والمسار الذي يعمل إطارٌ خاصّ: تحميل PIPViewController من
PIP.framework في زمن التشغيل، ونقلُ عرض الرسم الحقيقيّ لـ VLC إليه.
وليس في شيءٍ من هذا ما يعني أنّ VideoLAN تركت «صورة داخل صورة» دون حلّ. فـlibVLC يوفّر متحكّمًا لها، وكلا الغلافين يستعمله. غير أنّه مقصورٌ على iOS وtvOS في ملفّات بناء VLC نفسها، وملفُّ المصدر يقول ذلك في ترويسته، والصنفُ غائبٌ ببساطة عن شريحة macOS من الثنائيّ الذي أُصدره. فلا مسارَ أصليًّا على macOS في المحرّك لأستعمله.
ولذلك يأتي مسارُ الإطار الخاصّ خلف تفعيلٍ صريح، وكلُّ نداءٍ لرمزٍ خاصٍّ من رموز آبل في المكتبة يعيش في ملفٍّ واحدٍ بالضبط. لا لأنّ ذلك يجعله آمنًا لمتجر التطبيقات — فهو ليس كذلك، وذلك هو سببُ وجود التفعيل الصريح — بل لأنّ مدقّقًا يسأل عن الواجهات الخاصّة التي تمسّها هذه المكتبة ينبغي أن يقرأ ملفًّا واحدًا وينتهي.
وتفعل الواجهةُ الخلفيّة على iOS شيئًا شبيهًا على نطاقٍ أصغر: تُبلغ عمدًا أنّ «صورة داخل صورة» غير متاحة في المحاكي. فمحاكي AVKit قد يُبلغ عن متحكّمٍ عاملٍ بلا تحرّج بينما تبقى نافذة النظام سوداء. واختبارٌ ينجح في وجه ذلك ليس اختبارًا ناجحًا، بل كذبةٌ بعلامةٍ خضراء.
ثمّ يتسرّب التجريدُ في الاتّجاه الآخر. فاستخراجُ الهندسة التي احتجتُها من مسار vmem اقتضى إضافةَ مداخلَ إلى libVLC لا يملكها libVLC — أي إصدارَ محرّكٍ وسّعتُ سطحَه العامّ بلغة C، ثمّ التفاوضَ مع أرشيفاتٍ بُنيت قبل أن أخترعه.
المصافحة
swiftvlc_libvlc_pip_extensions_version()
تُرجع 0
أرشيفٌ صادرٌ سابقٌ لهذه الرموز
- تعاريف ضعيفة في وصلة Swift، وُجدت لينجح الربط لا غير
- الرجوع إلى نداءات الصيغة العامّة
- لا سبيل إلى إثبات القصّ ونسبة البكسل
تُرجع 3
أرشيفٌ مبنيٌّ من الشجرة المرقوعة
- تغلب تعاريف المحرّك القويّة
- نداء الصيغة الموسَّع، والهندسة ملتقَطةٌ دفعةً واحدة
- المقاس المرمَّز والمرئيّ والقصّ والنسبة تصل مجتمعةً أو لا تصل
ما أضافته كلّ مراجعة من الامتداد
- 1تهيئة vmem واعيةٌ بالهندسة، ولقطةٌ للوسيط وطوله تُؤخذ تحت قفل المشغّل
- 2لقطة تشغيلٍ بنوعٍ مستقلّ، فلا يكتب أرشيفٌ أحدث خارج مساحةٍ خصّصها عميلٌ أقدم
- 3تركيب الطبقات — ببوّابة إصدارٍ فقط، دون بديلٍ ضعيفٍ يُرجَع إليه
حين تكفّ عن قراءة الترويسات
تكفّ في لحظةٍ ما عن قراءة ترويسات محرّك C وتبدأ بقراءة شيفرته، وبعد ذلك بقليل تبدأ بتغييرها.
متى وصلت
- صدرت مع الإصدار 1.0
- كُتبت بعده في دفقةٍ واحدة
ما الذي تمسّه
- خرج الفيديو و«صورة داخل صورة»
- واجهة libVLC بلغة C
- نواة الدخل والمشغّل
- فاكّات التغليف — MP4، TS، HLS
- بنية البناء والاختبار
- Chromecast والبثّ الخارج
- اكتشاف UPnP
- avcodec
عدد الرقع التي تمسّ هذه المنطقة
من أين جاءت
- 9تعيد إنتاج إيداعاتٍ رسميّة
- 40 إيداعًا متمايزًا، مُسمّاةً في ترويسات الرقع — واثنتان من التسع تُفصحان عن تكييفٍ للنسخة المثبَّتة
- 16كُتبت هنا
- إصلاحاتٌ أصليّة، وسطحُ واجهةٍ جديد بلغة C، وترميماتُ بناءٍ لا سبب لحملها فوق
أين تقع
- 56
- ملفًّا من VLC عُدِّل
- 6
- ملفّات أُنشئتترويسةٌ عامّة، وثلاث ترويسات هندسةٍ لعازل العيّنات، واختبارا انحدارٍ أُضيفا إلى مجموعتَي VLC نفسها
شُحن الإصدار 1.0 بستّ رقع، ونزلت التسع عشرة الباقية بعده باثني عشر يومًا، في دفقةٍ واحدةٍ امتدّت ثمانية أيّام — وهو تقاربٌ أو تباعد بحسب حسن ظنّك. وأحسبه تقاربًا: فالإصدار 1.0 هو النقطة التي كففتُ عندها عن الالتفاف حول المحرّك وبدأتُ بإصلاحه.
وبعضها نقلٌ لإصلاحاتٍ رسميّةٍ وصلت بعد تثبيتي. وأوضحها سلسلةُ مؤقّت المشغّل: على
المراجعة المثبَّتة يستكمل المؤقّت الاستقراء بعد نقطة الإيقاف، فيظلّ مشغّلٌ
متوقّفٌ يبلّغ عن زمنٍ يتقدّم. ويظهر ذلك مباشرةً في Player.currentTime، ومن
القراءة نفسها يُضبط مرجعُ زمن «صورة داخل صورة»، فيكون العَرَضُ المرئيّ فيديو
متوقّفًا وشريطَ تقدّمٍ يزحف. وتحمل تلك الرقعة معها ثلاثَ إعاداتِ هيكلةٍ رسميّة
تعتمد عليها، حرفيًّا، بدل تكييف الإصلاحين الحقيقيّين يدويًّا على الشكل الأقدم —
وإحدى الثلاث هي التي تحوّل رايةَ الاستقراء إلى التعداد الذي يضيف إليه الإصلاحُ
الحقيقيّ حالةً. فإعادةُ أساسٍ يمكنك اشتقاقها من جديد خيرٌ من إعادة كتابةٍ عليك
التحقّق منها من جديد.
وبعضها لي. منها تحريرٌ مزدوج في فاكِّ التغليف التكيّفيّ في VLC.
تحرّر ISegment::toChunk() مصدرَ القطعة مرّتين على مسار فشل prepareChunk():
مرّةً صراحةً عبر recycleSource()، ومرّةً حين تحذف القطعة، لأنّ
~AbstractChunk() تحرّر المصدر الذي تملكه القطعة. ولقِطَع المقاطع لا تكون
إعادةُ التدوير قابلةً للتخزين فتمضي مباشرةً إلى delete، فيتوزّع التحرير الثاني
عبر جدول دوالٍّ افتراضيّة فوق ذاكرةٍ محرَّرة.
وقد نسخ إيداعٌ من سطرٍ واحدٍ في 2022 نداءَ إعادة التدوير من الفرع الذي تحته
مباشرةً إلى الفرع الذي فوقه، وإعادةُ التدوير في الفرع الأسفل صوابٌ تمامًا: إذ لم
تُرجع createChunk() شيئًا هناك، فلا قطعةَ توجد لتملك المصدر. أمّا في الفرع
الذي فوقه فالقطعةُ موجودة، فصارت النسخةُ تحريرًا ثانيًا. نسخ أحدُهم سطرًا صحيحًا
إلى فرعٍ لا يصلح فيه. وهو باقٍ في فرع VLC الرئيسيّ إلى اليوم، وثمّة بلاغٌ
مفتوح على متتبّع
VideoLAN يصف EXC_BAD_ACCESS متقطّعًا على iOS arm64 بعد نحو عشر دقائق من تشغيل
HLS، بمكدّسٍ ينتهي عند الهادم.
والدقائق العشر ليست مصادفة. فـprepareChunk() لا تعمل إلا للمقاطع المشفّرة، ولا
تفشل إلا حين يتعذّر الحصول على المفتاح. والمفاتيحُ محفوظةٌ في ذاكرةٍ مؤقّتة.
فتضرب العلّةُ عند تدوير المفاتيح أو بعد إخلاء تلك الذاكرة، لا عند البدء — وهذا
بعينه طابعُ يعمل عشر دقائق ثمّ ينهار في البلاغ.
مقاطعُ مشفّرةٌ وحدها، وتعذُّرُ مفتاح AES-128 وحده، وشبكةٌ متذبذبة، وجهازٌ حقيقيّ، وعشرُ دقائق. هذه العلّة لن تُسقط حزمةَ اختباراتك. ولم تُسقط حزمتي أنا كذلك، وأودّ أن أكون دقيقًا في ترتيب ما جرى: أصابها غيري أوّلًا، على إيداع المحرّك نفسه الذي أثبّته، وسجّلها في المشروع الأصل. وأنا وجدتُ الخلل بقراءة مخطّط الملكيّة، ثمّ أعدتُ إنتاجه على نحوٍ حتميّ. ورقعتي تضيف اختبارَ انحدارٍ إلى حزمة VLC التكيّفيّة نفسها، يعدّ التحريرات بدل أن يحرّر، فيُسقط التحريرُ المزدوج تأكيدًا بدل أن يقع في سلوكٍ غير معرَّف.
كتبتُ العروض قبل أن أكتب المكتبة
بنيتُ تطبيقات العرض أوّلًا — واحدًا لكلّ منصّةٍ أُصدر عليها، يستوفي كلَّ ميزةٍ عزمتُ على إصدارها، على ملفّ Matroska ذي الستّين ثانية — وبنيتُ حزمة الاختبارات إلى جانبها. وكُتب الاثنان كأنّ المكتبة التي تحتهما تعمل أصلًا. ولم تكن موجودة بعد. الموجودُ كان بيانًا قابلًا للتنفيذ لمعنى «أن تعمل»: شاشةٌ تشغّل بثًّا يحمل ثلاثة مسارات صوت وتتيح لك التنقّل بينها، ونافذةُ «صورة داخل صورة» تنجو من تنقّلٍ زمنيّ، وقائمةُ تشغيلٍ تتقدّم دون أن تُسقط حدثًا في الطريق.
ثمّ سدّ الوكلاءُ الفجوة. بنوا التطبيقات والحزمة وشغّلوها مئات المرّات، على محاكياتٍ يكون الغرضُ فيها الكثرة، وعلى عتادٍ حقيقيّ لما لا يفشل إلا عليه. قرأوا سجلّات الانهيار وفتحوا طلبات الدمج. وأكثرُ الشيفرة في هذه المكتبة خرج من تلك الحلقة، وهي شيفرةٌ جيّدة، لأنّها كُتبت وفق تعريفٍ للإنجاز موجودٍ سلفًا يمكن فحصه آليًّا.
والذي تغيّر هو ما صارت ساعاتي تذهب إليه. كفّت عن الذهاب إلى التنفيذ وصارت تذهب إلى التوصيف — إلى قول ما يعنيه «الصواب» بدقّةٍ تكفي ليقرّر غيري بلوغَه من عدمه. وإلى مسائل ذلك المستودع ذهب العمل، ولا شيء منه في العنوان أبدًا؛ وإنّما معايير القبول تحته هي التي أخذت الوقت: عشرةُ آلاف استبدالِ وسيطٍ نشط تحت أدوات الفحص، مع بياناتٍ وصفيّةٍ متأخّرةٍ ونداءاتِ هدم. واختبارُ تحمُّلٍ ممتدّ للتنقّل الزمنيّ على جهازٍ حقيقيّ، مع الاحتفاظ بمنشأ التخصيص. وصفٌّ واحدٌ يجمع واجهتَي «صورة داخل صورة» الخلفيّتين كلتيهما، كي لا تستطيع واجهةٌ ناجحة أن تخفي أخرى فاشلة. وكتابةُ ذلك كتابةً جيّدة أعسرُ من كتابة الإصلاح، وهي الجزءُ الذي لا يُفوَّض.
وحلقةٌ تعمل مئات المرّات لا تساوي شيئًا إلا بمقدار قدرتها على أن تقول «لا»، وقد تعلّمتُ ذلك تعلُّمًا مكلفًا. فقد تبيّن أنّ دفعةَ اختباراتٍ كنتُ قد قبلتُها تضمّ ستّة عشر اختبارًا لا سبيل لها أصلًا إلى بلوغ الشرط الذي تنتظره. كانت تنجح منذ أشهرٍ دون أن تختبر شيئًا. والنموذجُ سينتج إصلاحًا معقولًا واختبارًا معقولًا له في المرّة نفسها، ولا يساوي أيٌّ منهما شيئًا حتى تكسر الإصلاح وترى الاختبار يفشل.
ولذلك فكلُّ ما بنيتُه بعد ذلك إنّما وُجد ليجعل ذلك الفحص رخيصًا. فكلُّ ما يحمل ثقلًا يحمل في متن إيداعه ملاحظةَ «أحمر-أخضر» تسجّل أنّ الإصلاح نُقض وأنّ الاختبار شوهد وهو يفشل. وفحصُ تغطيةٍ يبلّغ أيَّ الرقع لا يحويها الثنائيّ قيدَ الاختبار، لأنّ التكامل المستمرّ يربط محرّكًا صادرًا، والرقعةُ في الشجرة ليست رقعةً في المُخرَج. وبوّابةُ الإصدار تُفصح عن حقيقتها: هذه لا تشغّل الاختبارات — بل يشغّلها إنسان، على عتاد. وإنّما تفرض أن تكون النتائج موجودة، وأن يكون كلُّ صفٍّ مطلوبٍ قد أُجري ونجح، وأنّها تصف هذا المُخرَج لا مُخرَجًا أسبق.
وقد استقرّ الحدُّ في الموضع نفسه كلَّ مرّة. فالكثرةُ والصبر يُفوَّضان تفويضًا تامًّا، أمّا الملكيّة فلا. ترتيبُ الأقفال، واللحظةُ التي يموت فيها كائن C فعلًا، وما تتوقّعه AVKit منك ممّا لم يكتبه أحدٌ قطّ — في هذه كنتُ أتلقّى شيئًا معقولًا وخاطئًا، وأحبُّ الأجوبة الخاطئة إليها مهلةٌ زمنيّة، والجوابُ الحقيقيّ قاعدةٌ في مَن يملك ماذا. والمهلةُ تنجح، ثمّ تفشل بعد شهر، على شبكة شخصٍ آخر. وقراءةُ شيفرة VLC حتى تُفهم الآليّة هي ما لم أستطع تسليمه، وهي أيضًا ما أنتج الرقع.
وقد لزم النصفان كلاهما، والتحريرُ المزدوج هو الدليل — في الاتّجاه الذي لم أتوقّعه. فالحلقةُ لم تجده. وجده بلاغُ انهيارٍ من شخصٍ آخر، على الإيداع الذي أثبّته، وإغلاقُه اقتضى الجلوس إلى شيفرة VLC حتى يستقيم توزيعُ جدول الدوالّ الافتراضيّة. أمّا ما كسبته الحلقة فكلُّ ما تلا ذلك: إعادةُ الإنتاج الحتميّة، وتشغيلُ أدوات الفحص، وأربعُ مئة مرورٍ أظهرت أنّ الإصلاح صامد. التشخيصُ كان لي، وأمّا البرهانُ فلا.
ما كلّفه ذلك
أربعةُ أشهرٍ ونصف حتى 1.0، والفاتورة ما تزال تصل.
ومجموعةُ الرقع أكبرُ بندٍ فيها، وهي التزامٌ لا إنجاز. فسلسلةُ رقعٍ مرتّبة على
مراجعةٍ مثبَّتة تعني إعادةَ أساسٍ كلّما تحرّك التثبيت، والتثبيتُ لا يضمن لك
عالمًا مستقرًّا أصلًا: إحدى تلك الرقع موجودةٌ لا لشيء إلا أنّ إصدارًا أحدث من
Autoconf صار يناول مترجمَ C رايةَ معيارٍ غير التي يناولها مترجمَ Objective-C،
فعجز libtool عن استنتاج الوسم، فماتت كلُّ شرائح آبل في test/، بعد أن كان
libvlccore وlibvlc قد رُبطا كلاهما. وهي المصادر نفسها التي بُنيت قبل ذلك
بأسبوعين.
ويُوزَّع المحرّك أرشيفًا ساكنًا، وذلك القرارُ الواحد في التحزيم يُحسب عليّ مرّتين. مرّةً بوصفه مشكلة ترخيص: فـlibVLC تحت LGPL 2.1، والربطُ الساكن يدفع المستهلك نحو البند الذي يتوقّع منك توزيع ملفّات الكائنات ليتمكّن مستخدموك من إعادة الربط، وتطبيقُ متجر التطبيقات لا يستطيع ذلك. وثمّة مسألةٌ مفتوحة تطلب منّي إصدارَ xcframework ديناميكيّ بدلًا من ذلك، سجّلها شخصٌ لا يعمل على هذا وكان كريمًا في صياغتها — الترخيصُ وحده هو ما يدفع في الاتّجاه الآخر — ولم أجب عنها. فالربطُ الديناميكيّ سيُنتج ثنائيًّا أكبر، ولم أحسم بعد ما تساويه تلك المقايضة.
ومرّةً بوصفه مشكلةً هندسيّة، لأنّ الأرشيف الساكن لا هويّة له عند التحميل.
لماذا يهمّيُوزَّع المحرّك أرشيفًا ساكنًا، والأرشيف الساكن لا هويّة له عند التحميل. وصورتان تربطانه كلتاهما تُنتجان زمنَي تشغيلٍ كاملين لـ libVLC في عمليّةٍ واحدة — سجلَّي إضافات، وخمس عشرة فئة Objective-C بلا مجال أسماء مُعرَّفة مرّتين، إحداهنّ اسمها AoutWrapper وحسب. ولا يفشل الربط في شيء. وإنّما يختار زمن التشغيل واحدةً من كلّ زوج ولا يخبرك أيّها.
الشكل المدعوم
- التطبيق
- FeatureAساكن
- FeatureBساكن
- MediaCoreديناميّ
- SwiftVLC
- libvlc.a
ما يعدّه التكامل المستمرّ
_libvlc_new
| الصورة المحمَّلة | تُعرّفه |
|---|---|
| ملفّ التطبيق | 0 |
| FeatureA | 0 |
| FeatureB | 0 |
| MediaCore.framework | 1 |
واحدةٌ لا غير، ولا بدّ أن تكون هي.
وبقيّةُ الدفتر باختصار. مسارُ «صورة داخل صورة» الذي يرسم على وجهه الصحيح على macOS لا يمكن أن يدخل متجر التطبيقات، أي أنّ أفضل نسخةٍ من ميزةٍ بنيتُها هي نسخةٌ لا أستطيع إصدارها. ويُوزَّع المحرّك مجرّدًا من رموز التنقيح وبلا dSYM، فالانهيار داخل libVLC يعطي كلَّ من يستعمله اسمَ دالّةٍ بلا رقم سطر. وصار تأهيلُ الأجهزة الفعليّة مصفوفةً من ثلاثةٍ وخمسين صفًّا، وعُدّةَ اختبارٍ ملفُّ اختبارها وحده أطولُ من أيّ شيءٍ كتبتُه في المكتبة — لأنّ ما ينكسر هنا ينكسر على العتاد، وما سواه لا يُقبل دليلًا.
وإن كنتَ تفكّر في تغليف مكتبة C كبيرة، فارصد ميزانيةً لشيفرتها لا لترويساتها. فسطحُ الواجهة يبدو كأنّه العمل. وهو محدودٌ بالترويسة، وهو الشكل نفسه في كلّ مرّة. أمّا ما يكلّف شهورًا فذلك الجزء الذي تكون فيه افتراضاتُ المحرّك وضماناتُ لغتك صحيحةً معًا ومتنافيةً معًا، ولا يقع في مستودعٍ تستطيع تحريره إلا أحدهما.
كففتُ عن استهلاك المحرّك وبدأتُ بامتلاكه، والمحرّكُ الذي تملكه يناولك ترخيصَه وتحزيمَه وعللَه معًا، كتبتَ منها شيئًا أو لم تكتب. تلك هي المقايضة. وسأعقدها من جديد يوم الاثنين. غير أنّها التزامٌ قائم، لا محطّةٌ تجتازها.
وليس لديّ مجموعٌ أعطيكه. فـ«صورة داخل صورة» على iOS ما تزال تُطفئ نفسها عند تنقّلٍ زمنيٍّ عاديّ — والعلّةُ مفتوحةٌ على المتتبّع وأنا أكتب هذا، بإصلاحٍ مدموجٍ لم يُثبَت بعد على العتاد، سجّلها شخصٌ لم ألقه قطّ. وهي الميزةُ التي غادرتُ VLCKit لأنالها. بضعٌ وعشرون رقعةً في محرّك غيري، والشيءُ الذي بدأ هذا كلَّه هو الشيءُ الذي ما يزال مفتوحًا.