Steven
Steven4 นาทีที่อ่าน

แยกการโทรออกจากแอปที่แค่เปิดค้างไว้

GeekBye สังเกตได้ว่าคุณเข้าร่วมการประชุมวิดีโอแล้ว และเสนอบันทึกมันให้ด้วยคลิกเดียว การตรวจจับกลายเป็นครึ่งที่ง่าย — Swift binary ที่อ่านชื่อหน้าต่างทุกสิบวินาที ครึ่งที่ยากคือความแม่นยำ: ไม่ยิงตอน Zoom แค่เปิดค้างอยู่, ไม่เตือนสำหรับการประชุมที่คุณกำลังบันทึกอยู่แล้ว, และไม่ mute ไมค์ในการโทรที่คุณอยู่จริง ๆ สามรีลีส และแต่ละอันคือ guard ที่ต้องเรียนรู้ที่จะไม่เอาชนะตัวเอง

วิศวกรรม
macOS
Desktop
GeekBye รีลีส
แยกการโทรออกจากแอปที่แค่เปิดค้างไว้

GeekBye มีฟีเจอร์ที่ฟังดูกระจอกแต่ไม่ใช่: มันสังเกตได้ว่าคุณเข้าร่วมการประชุมวิดีโอเมื่อไร และเสนอ, ด้วยคลิกเดียว, ที่จะเริ่มบันทึกมัน คำโปรยคือประโยคเดียว ส่วน implementation คือสามรีลีสของการเรียนรู้ว่าปัญหาที่น่าสนใจไม่เคยเป็น «ตรวจจับการประชุม» — มันคือ «แม่นเรื่องนั้น» แม่นว่าคุณอยู่ในสายจริง ๆ หรือแค่เปิดแอปค้างไว้ แม่นว่าไม่ถามซ้ำสอง แม่นว่าไม่ mute ไมค์ผิดตัว แต่ละอันในสามรีลีสนั้น, ที่แก่นของมัน, คือ guard ที่ต้องถูกสอนให้ไม่สะดุดขาตัวเอง

ชื่อหน้าต่าง, ไม่ใช่ process

การตัดสินใจแรกและมีน้ำหนักที่สุดถูกเขียนไว้ใน commit message ในฐานะคำประกาศดีไซน์ ไม่ใช่เชิงอรรถ:

Detection uses window title matching (not process detection) to distinguish "app is open" from "in an active call."

ความต่างนั้นคือทั้งฟีเจอร์ ถ้าคุณตรวจจับการประชุมด้วยการดูว่าแอปไหนกำลัง_รัน_ คุณจะยิงทันทีที่ Zoom เปิดขึ้น — และ Zoom เปิดค้างอยู่บนเดสก์ท็อปมากมายทั้งวันโดยไม่มีการโทรสักสาย ดังนั้น GeekBye ไม่เฝ้าดู process บน macOS มันรัน native Swift binary เล็ก ๆ, MeetingMonitor, ที่ทุกสิบวินาทีเรียก CGWindowListCopyWindowInfo สำหรับหน้าต่างบนจอ และอ่านชื่อของแต่ละอันผ่าน kCGWindowName หน้าต่างที่ชื่อ "Zoom Meeting" แปลว่าคุณอยู่ในสาย; หน้าต่าง "Zoom Workplace" แปลว่าแอปแค่เปิดอยู่ สัญญาณไม่ใช่ «Zoom ยังอยู่ไหม» แต่คือ «หน้าต่างบนจอของคุณดูเหมือนสายที่กำลังโทรอยู่หรือเปล่า»

ช่วงเวลา poll เป็นค่าคงที่แข็ง ๆ — pollIntervalSeconds: Double = 10.0 — บวกการตรวจทันทีหนึ่งครั้งตอน startup ดังนั้นการประชุมถูกสังเกตเห็นภายในราวสิบวินาทีหลังหน้าต่างของมันปรากฏ (นั่นเป็นค่าในโค้ด ไม่ใช่ latency ที่วัดได้; ให้ถือ «ไม่เกิน ~10 วิ» เป็นเลขคณิต ไม่ใช่ benchmark)

การประชุมบน browser ต้องการอีกขั้น เพราะหน้าต่าง Chrome ที่ชื่อ "Meet - Weekly Sync" นั้นมีลุ้น แต่ความแน่นอนอยู่ใน URL ดังนั้นเมื่อแอปหน้าสุดเป็น browser ที่รู้จัก — Chrome, Safari, Edge, Brave, และอีกกำมือที่ระบุด้วย bundle id — binary ดึง URL ของแท็บที่ active ผ่าน AppleScript แล้วเทียบกับ pattern อย่าง meet.google.com/, zoom.us/j/, และ teams.microsoft.com/ AppleScript เข้าไปในอีกแอปอาจค้างได้ ดังนั้น call นั้นรันบน background DispatchQueue ที่คุมด้วย DispatchSemaphore พร้อม timeout 3 วินาที; browser ที่ค้างจะ timeout แทนที่จะแช่แข็ง detector ทั้งตัว

ส่วนที่ยากคือการไม่ยิง

นี่คือส่วนที่ทำให้ผมประหลาดใจตอนอ่านรายการ pattern การตรวจจับสายคือการ match string ไม่กี่ครั้ง การ_ไม่_ตรวจจับสาย — การไม่ยิงตอนคุณไม่อยากให้มันยิง — ต่างหากที่ความใส่ใจทั้งหมดไปลง รายการ pattern ของชื่อหน้าต่างอ่านเหมือนชุดของการยกเว้นอย่างจงใจ:

  • Google Meet match "Meet - " และรุ่น en-dash ของมัน "Meet – ", แต่ไม่ match string เปล่า ๆ "Google Meet" — เพราะนั่นคือหน้า landing ที่คุณเปิดก่อนโทร
  • Zoom match "Zoom Meeting" และ "Zoom Webinar", แต่ไม่ match "Zoom Workplace" หรือ "Zoom" เปล่า ๆ — แอปที่นั่งเปิดอยู่
  • Teams match "Meeting with", "Meeting in", " | Call", " | Meeting" — รูปทรงของชื่อระหว่างสาย, ไม่ใช่ client ที่ว่างอยู่
  • Slack huddle, Webex room, และ WhatsApp call ต่างได้ชิ้นส่วนเฉพาะ-ระหว่างสายของตัวเอง

ทุกอันในนั้นคือการตัดสินใจที่จะเงียบไว้ในกรณีปกติที่แอปเปิดอยู่แต่คุณไม่ได้อยู่ในการประชุม เป้าหมายดีไซน์ไม่ใช่ recall สูงสุด; มันคือการไม่เป็นเครื่องมือที่เด้ง banner «อยากบันทึกไหม?» ขึ้นมาตอนคุณกำลังอ่าน Slack false positive ตรงนี้แพงในแบบที่ false negative ไม่แพง: พลาดสายไปแล้ว user ก็เริ่มบันทึกเอง; ยิงพลาดบนแอปที่เปิดอยู่ user ก็เรียนรู้ที่จะเมิน banner ของคุณ heuristic ทั้งชุดถูกจูนไปทางให้ความล้มเหลวแบบที่สองเกิดยาก

การเอ่ยขีดจำกัดของ heuristic ที่เรียบง่ายขนาดนี้เป็นเรื่องซื่อสัตย์ pattern เป็นภาษาอังกฤษล้วน — OS ที่ localize หรือแอปประชุมที่ไม่ใช่อังกฤษจะไม่ match, ข้อจำกัด v1 ที่ยอมรับกัน และเพราะชื่อหน้าต่างของแอปอื่นมองเห็นได้เฉพาะเมื่อได้รับสิทธิ์ Screen Recording, การตรวจจับจึงเสื่อมเงียบ ๆ เหลือศูนย์ถ้าไม่มีมัน เรียบง่าย, อ่านออก, และไม่สมบูรณ์ในแบบที่ทีมเขียนจดไว้แทนที่จะซ่อน

คำเสนอ, และกลไกที่กันมันไม่ให้จู้จี้

เมื่อ pattern match, GeekBye แสดง banner เล็ก ๆ ไร้กรอบที่ลอยบนสุดเสมอที่มุมจอ — หน้าต่างของมันเอง, ไม่ใช่ notification ของ OS — พร้อมปุ่ม Start Recording คลิกมันแล้ว IPC message (meeting:start-recording) ซ่อน banner, เรียกหน้าต่างหลักขึ้นมา, และบอก renderer ให้เริ่ม session ปัดมันทิ้งแล้ว banner ก็หายไป

แต่รุ่นไร้เดียงสาของสิ่งนี้คงทำให้บ้า ชื่อเปลี่ยนตลอดเวลาระหว่างสาย — ผู้เข้าร่วมเข้ามา, คุณเปลี่ยนชื่อการประชุม, การแชร์จอสลับชื่อหน้าต่าง — และการเปลี่ยนแต่ละครั้งจะจุด detector ที่ผูกกับ string เป๊ะ ๆ ให้ยิงใหม่ ดังนั้น detector ใน MeetingDetector.ts พก state machine เล็ก ๆ ที่งานทั้งหมดของมันคือความยับยั้งชั่งใจ:

  • meetingId ที่เสถียรต่อแพลตฟอร์ม ฝั่ง Swift ปล่อย id อย่าง google-meet-active, จงใจ_ไม่_ให้มาจากชื่อหน้าต่างที่ผันผวน, เพื่อให้การเข้า, ออก, และเปลี่ยนชื่อภายในสายเดียวไม่ถูกอ่านเป็นการประชุมใหม่
  • dismissedMeetings: Set<string> ปัด banner แล้ว id ของการประชุมนั้นถูกเมิน — จนกว่าการประชุมจะจบจริง, ณ จุดนั้น id เคลียร์เพื่อให้การประชุมจริง_อันถัดไป_เตือนได้ใหม่
  • boolean notificationActive banner เดียวต่อครั้ง, เพื่อให้ Slack huddle ที่เด้งขึ้นตอนคุณอยู่ใน Meet อยู่แล้ว ไม่ซ้อนคำเตือนอันที่สอง

นั่นแหละคือฟีเจอร์นี้จริง ๆ: การตรวจจับนิดหน่อย และ «อย่ากวนใจ» เยอะ ๆ ซึ่งเป็นเหตุผลเป๊ะ ๆ ว่าทำไมสองรีลีสถัดมาถึงมีอยู่ — เพราะ guard สองอันในนั้นกลายเป็นเอาชนะตัวเอง

การ mute ที่ mute การประชุมจริงของคุณ (v1.8.18)

การบันทึกมีปุ่ม mute, และ implementation แรกรับ «mute» ตามตัวอักษรที่สุด ship ใน v1.8.15, มัน mute ไมค์ด้วยการตั้ง input volume ของระบบปฏิบัติการเป็นศูนย์:

osascript -e "set volume input volume 0"

พร้อมกับ volume ก่อนหน้าที่เซฟลงฐานข้อมูลเพื่อคืนค่าได้, ตัวเทียบเท่าฝั่ง Windows ผ่าน waveInSetVolume, และตาข่ายกันภัย before-quit เพื่อให้แน่ใจว่ามัน un-mute โค้ดถึงกับบรรยายมันเป็นฟีเจอร์ — mute «ที่ระดับ TWO», ระบบและแอป, เพื่อให้ไมค์ถูกบล็อกสำหรับ_ทุก_แอป

วลีสุดท้ายนั้นคือบั๊กที่พูดออกมาเป็นคำอวด การบล็อกไมค์สำหรับทุกแอปแปลว่าบล็อกมันสำหรับสาย Zoom หรือ Meet ที่คุณนั่งอยู่จริง ๆ คุณกด mute ใน GeekBye เพื่อหยุด_มัน_ไม่ให้ได้ยินคุณ แล้วคุณก็เงียบไปในการประชุมจริงของคุณ การแก้ v1.8.18 คือการลดขั้น: เลิกแตะ input volume ของ OS ทั้งหมด, และ mute เฉพาะ audio pipeline ของ GeekBye เอง ใน Listen mode, RealtimeAudioCaptureService แทน frame ไมค์ที่ mute ด้วย buffer ศูนย์เพื่อให้ความเงียบไหลไปที่ mixer; ใน video mode, ScreenRecordingService ตั้ง GainNode ของ Web Audio เป็นศูนย์ การ mute ทั้งระบบ, การกู้ volume ที่เซฟไว้, กลไกข้ามแพลตฟอร์มทั้งชุด — ลบหมด mute ควรแปลว่า «แอปนี้หยุดฟัง», ไม่ใช่ «ไมค์ของคุณปิดสำหรับทุกคน»

guard ที่ reset ตัวเอง (v1.8.19)

guard เอาชนะตัวเองอีกอันคือ notificationActive บนกระดาษมันตอบ «อย่าโชว์ banner ถ้ามีอันหนึ่งขึ้นอยู่» ไปแล้ว — และมันดูเหมือนควรจะครอบ «อย่าเสนอบันทึกการประชุมที่ฉันกำลังบันทึกอยู่แล้ว» ด้วย มันไม่ได้ครอบ, และ commit ที่แก้มันอธิบายว่าทำไมอย่างแม่นยำ:

The notificationActive flag resets when the meeting detector cycles through ended/detected states, causing notifications to reappear mid-recording.

detector คือ poll สด ๆ; ตลอดสายยาว ๆ มันอาจอ่าน «ended» ชั่วครู่แล้วอ่าน «detected» อีกที — ชื่อกะพริบ, หน้าต่างหลุดจอชั่วขณะ — และแต่ละรอบ reset flag ดังนั้น guard เลยเงียบ ๆ ขึ้นลำใหม่ให้ตัวเองตอนคุณกำลังบันทึก, และ banner ก็จะโผล่กลับมาเสนอบันทึกสายที่คุณกำลังบันทึกอยู่แล้ว การแก้เลิกเชื่อ flag ในเครื่องและถามแหล่งความจริงแทน: callback recordingActiveChecker, inject จาก main.ts และต่อสายไปที่สถานะ isRealtimeTranscriptionActive จริง, ถูกตรวจก่อน notification ใด ๆ จะโชว์

// Don't show notification if recording/transcription is already active
if (this.recordingActiveChecker?.()) {
  return
}

บทเรียนเล็กและกว้าง: guard ที่สถานะเป็นของคุณ อาจถูก reset จากใต้เท้าคุณด้วย control flow ของคุณเอง guard ที่อ่านสถานะจริงที่มันปกป้อง ทำแบบนั้นไม่ได้ notificationActive กำลัง track proxy ของ «เรากำลังบันทึกไหม»; การแก้ถามว่าเรากำลังบันทึกอยู่หรือเปล่า

สามสิ่งที่รีลีสเหล่านี้สอนเรา

  1. ตรวจจับสถานะที่คุณหมายถึง, ไม่ใช่สถานะที่มองเห็นง่าย «แอปกำลังรัน» ตรวจง่ายและผิด «มีหน้าต่างที่ดูเหมือนสายที่กำลังโทรอยู่» ยากขึ้นนิดและถูก ช่องว่างระหว่างสองอันนั้นคือทั้งฟีเจอร์
  2. สำหรับฟีเจอร์เชิงรุก ความแม่นยำคือตัวผลิตภัณฑ์ ไม่มีใครจำการประชุมที่คุณตรวจจับถูก; เขาจำสามครั้งที่คุณขัดจังหวะเขาตอนเขาไม่ได้อยู่ในสาย ข้อยกเว้น pattern และ state machine กันจู้จี้ไม่ใช่การขัดเงา — มันคือความต่างระหว่าง banner ที่ช่วยได้ กับ banner ที่คุณฝึกตัวเองให้เมิน
  3. guard ที่ reset ตัวเองได้ไม่ใช่ guard สองรีลีสตามมามีรูปทรงเดียวกัน: กลไกความปลอดภัยที่อ้าง proxy ที่มันควบคุมเอง (volume ที่เซฟไว้, boolean ในเครื่อง) แทนสถานะจริง (ไมค์ของ OS, สถานะ transcription) จ่อ guard ไปที่ความจริง, ไม่ใช่ที่สำเนาของมัน

สำหรับบทก่อนหน้าในเรื่องยุค v1 ดูเอา backend ออกจากเส้นทางอัปโหลด (v1.8.11–v1.8.13); และสำหรับภาพรวมทั้งเส้นเรื่อง ดูกายวิภาคของการส่งมอบซอฟต์แวร์ให้สมบูรณ์แบบ

บทความที่เกี่ยวข้อง

สามกริยาที่ทำให้ Web Audio ยังมีชีวิต
Steven
Steven5 นาทีที่อ่าน

สามกริยาที่ทำให้ Web Audio ยังมีชีวิต

GeekBye สอง point release ห่างกันสองเดือนและอยู่คนละไฟล์ สอนโค้ดเสียงของเราด้วยบทเรียนเดียวกันจากปลายสองด้านตรงข้าม: เลิกมอง AudioContext ของ browser ว่าเป็นของใช้แล้วทิ้ง release หนึ่งเรียนรู้ที่จะ resume() context ที่ macOS แอบ suspend ไปกลางการอัด; อีกอันเรียนรู้ที่จะ suspend() แทน close() เพื่อให้ session ที่ต่อกันเป็นพรืดเลิกพุ่งชนเพดานราวหก context ของ Chromium resume, suspend, close — นั่นคือทั้งเรื่อง

วิศวกรรม
Audio
Desktop
เอา backend ออกจากเส้นทางอัปโหลด
Steven
Steven5 นาทีที่อ่าน

เอา backend ออกจากเส้นทางอัปโหลด

GeekBye บันทึกหน้าจอของคุณและเซฟวิดีโอลง Google Drive ของคุณ เวอร์ชันแรกส่งทุกการบันทึกผ่านเซิร์ฟเวอร์ของ GeekBye เองระหว่างทางไปที่นั่น; อีกหนึ่งรีลีสถัดมา ไฟล์ก็วิ่งตรงจากเครื่องของคุณไป Drive และ backend ถูกลดขั้นเหลือเพียงถือ pointer หนึ่งตัว ส่วนที่น่าสนใจคือเวอร์ชัน «ตรง, resumable» นั้นจริง ๆ แล้วบรรจุโค้ดน้อยแค่ไหน — เพราะความ resumable มาจากการลบ proxy ทิ้ง ไม่ใช่จากการเขียนมันขึ้นมา

วิศวกรรม
สถาปัตยกรรม
Desktop
ถือกำเนิดและถูกแก้ในสิบสองนาที
Steven
Steven4 นาทีที่อ่าน

ถือกำเนิดและถูกแก้ในสิบสองนาที

changelog ของ GeekBye v1.8.10 บอกว่ามันแก้ crash ตอนแก้ไข keyboard shortcut มันแก้จริง — แต่ crash นั้นถูกใส่เข้ามาและถูกแก้ใน pull request เดียวกัน ห่างกันสิบสองนาที และไม่เคยไปถึงผู้ใช้แม้แต่คนเดียว เรื่องจริงคือปฏิกิริยาลูกโซ่ด้านความน่าเชื่อถือที่ผลิตมันออกมา: การเปลี่ยนแปลงเล็ก ๆ ที่ถูกต้องเรื่องว่าจะ suspend shortcut เมื่อไหร่ และ React teardown bug ที่หล่นออกมาจากโค้ดบรรทัดเดียวที่ตั้งใจให้ editor ปลอดภัยขึ้น

วิศวกรรม
React
Desktop