เพิ่มประสิทธิภาพเสียงโดยใช้ตัวถอดรหัสในกระบวนการ

ตั้งแต่ Android 17 (ระดับ API 37) เป็นต้นไปและอัปเดตผ่านการอัปเดตระบบ Google Play (โมดูล APEX หลัก) Android จะเปิดตัวตัวถอดรหัสเสียงซอฟต์แวร์ในกระบวนการสำหรับรูปแบบเสียงที่บีบอัด ซึ่งรวมถึง Opus และ AAC

การเรียกใช้ตัวถอดรหัสเสียงภายในกระบวนการของแอปโดยตรงแทนที่จะใช้กระบวนการของระบบแซนด์บ็อกซ์แยกต่างหากจะช่วยให้แอปสามารถลดเวลาในการตอบสนองของการถอดรหัสเสียงได้ประมาณ 40% ลดการใช้ CPU และยืดระยะเวลาการใช้งานแบตเตอรี่ในระหว่างการเล่นสื่อ การส่งข้อความเสียง และการเล่นเกมอย่างต่อเนื่อง

การถอดรหัสแบบนอกกระบวนการเทียบกับการถอดรหัสแบบในกระบวนการ

ในอดีต Android เรียกใช้ตัวถอดรหัสสื่อซอฟต์แวร์แพลตฟอร์มทั้งหมดภายใน daemon ของระบบที่แยกไว้และอยู่ในแซนด์บ็อกซ์ (mediaswcodec) เพื่อปกป้องแอปและ ระบบปฏิบัติการจากบิตสตรีมสื่อที่มีรูปแบบไม่ถูกต้อง อย่างไรก็ตาม แซนด์บ็อกซ์นอกกระบวนการ ทำให้เกิดค่าใช้จ่ายด้านประสิทธิภาพที่วัดได้ ดังนี้

  • เวลาในการตอบสนองของการสื่อสารระหว่างกระบวนการ (IPC): ทุกเฟรมอินพุตเสียง ที่จัดคิวไว้ในตัวถอดรหัสและทุกบัฟเฟอร์เสียง PCM ที่ถอดรหัสแล้วที่ส่งคืนต้องมีการ ซีเรียลไลซ์ IPC ของ Binder ข้ามกระบวนการและการสลับบริบท
  • ค่าใช้จ่ายของหน่วยความจำที่ใช้ร่วมกัน: การส่งต่อบัฟเฟอร์ระหว่างกระบวนการต้องมีการซิงโครไนซ์หน่วยความจำที่ใช้ร่วมกัน (C2Buffer) และการจัดการแคช
  • การแย่งชิงการจัดกำหนดการ: เมื่อ CPU มีภาระงานหนัก การแย่งชิงการจัดกำหนดการเธรด ระหว่างกระบวนการของแอปกับ mediaswcodec อาจทำให้บัฟเฟอร์ ขาดแคลนและเสียงขาดหายในไปป์ไลน์เสียงที่มีเวลาในการตอบสนองต่ำ (เช่น DAW, การสื่อสารผ่าน VoIP และเกมแบบอินเทอร์แอกทีฟ)

การเรียกใช้ตัวถอดรหัสสื่อในกระบวนการจะช่วยแก้ปัญหาคอขวดด้านประสิทธิภาพต่อไปนี้ได้โดยตรง

  • ลดเวลาในการตอบสนองของ IPC: ข้ามธุรกรรม IPC ของ Binder และการสลับบริบท ซึ่งจะช่วยลดเวลาในการตอบสนองของการถอดรหัสตั้งแต่ต้นจนจบได้ประมาณ 40%
  • ลดค่าใช้จ่ายของบัฟเฟอร์: ส่งบัฟเฟอร์เสียงโดยตรงภายในพื้นที่หน่วยความจำของแอปในเครื่องโดยไม่ต้องมีการแมปหน่วยความจำที่ใช้ร่วมกันข้ามกระบวนการ
  • ป้องกันการสั่นของกำหนดการ: ดำเนินการถอดรหัสในเธรดที่มีลำดับความสำคัญของแอปเอง (เช่น เธรด AAudio หรือ Oboe แบบเรียลไทม์) เพื่อป้องกันไม่ให้เสียงขาดหาย และบัฟเฟอร์ขาดหายเมื่อระบบมีภาระงานหนัก

ความปลอดภัยของหน่วยความจำด้วย Rust

ในอดีต Android แยกตัวถอดรหัสซอฟต์แวร์ในกระบวนการแยกต่างหาก (mediaswcodec) เนื่องจากตัวถอดรหัสประมวลผลบิตสตรีมที่ไม่น่าเชื่อถือจากไฟล์สื่อภายนอก จึงทำให้การใช้ประโยชน์จากการเสียหายของหน่วยความจำ (เช่น การบัฟเฟอร์เกินขอบเขตที่กำหนด) เป็น ข้อกังวลด้านความปลอดภัยที่สำคัญ

Android สามารถย้ายตัวถอดรหัสซอฟต์แวร์ไปยังกระบวนการของแอปการโทรได้อย่างปลอดภัย โดยการนำไปใช้ในภาษาที่ปลอดภัยต่อหน่วยความจำ เช่น Rust หรือแยกด้วย การแยกข้อบกพร่องแบบเบา (LFI)

Android มีตัวถอดรหัสที่ใช้ใน Rust แบบเพียวหรือได้รับการปกป้องโดย Safe Rust Foreign Function Interface (FFI) harnesses เพื่อลดค่าใช้จ่ายของแซนด์บ็อกซ์แบบแยกกระบวนการสำหรับ AAC (c2.android.inproc.aac.decoder) อย่างปลอดภัย

เราถือว่า Rust ปลอดภัยสำหรับการถอดรหัสในกระบวนการเนื่องจากเหตุผลต่อไปนี้

  • ความปลอดภัยของหน่วยความจำขณะคอมไพล์: การตรวจสอบความเป็นเจ้าของ การยืม และอายุการใช้งานที่เข้มงวดของ Rust ช่วยให้มั่นใจได้ว่าจะเข้าถึงหน่วยความจำไม่ได้หลังจากที่ระบบปล่อยหน่วยความจำนั้นแล้วหรือมีการเปลี่ยนแปลงขณะที่แชร์
  • การกำจัดช่องโหว่ที่พบบ่อย: Rust ป้องกันไม่ให้เกิดข้อผิดพลาดเกี่ยวกับบัฟเฟอร์ล้นฮีป การโจมตีสแต็ก การอ่านและการเขียนอาร์เรย์ที่อยู่นอกขอบเขต ข้อผิดพลาดในการใช้หลังจากปล่อย และช่องโหว่ที่ทำให้เกิดการปล่อยหน่วยความจำซ้ำในเวลาคอมไพล์
  • ไม่มีค่าใช้จ่ายในการแซนด์บ็อกซ์รันไทม์: เนื่องจากความปลอดภัยของหน่วยความจำได้รับการพิสูจน์ทางคณิตศาสตร์ ในเวลาคอมไพล์และได้รับการยืนยันผ่านการตรวจสอบขอบเขต ดีโคดเดอร์ Rust จึง ทำงานด้วยความเร็วแบบเนทีฟภายในกระบวนการของแอปโดยไม่ต้องมีการเปลี่ยนตารางหน้า หรือบริบทแซนด์บ็อกซ์ของฮาร์ดแวร์

ความปลอดภัยของหน่วยความจำด้วยการแยกข้อบกพร่องแบบเบา (LFI)

สำหรับตัวถอดรหัสเสียง C/C++ รุ่นเดิมที่ซับซ้อนซึ่งยังไม่ได้เขียนใหม่ใน Rust โดยเฉพาะตัวถอดรหัส Opus (c2.android.inproc.opus.decoder ขับเคลื่อนโดย libopus) Android จะให้ความปลอดภัยในกระบวนการโดยใช้การแยกข้อบกพร่องแบบ Lightweight (LFI)

LFI ถือว่าปลอดภัยสำหรับตัวถอดรหัส C/C++ ด้วยเหตุผลต่อไปนี้

  • การแซนด์บ็อกซ์หน่วยความจำเชิงเส้นที่ได้รับการป้องกันด้วยฮาร์ดแวร์: LFI จะคอมไพล์ไลบรารีโคเดก C/C++ เป็นรหัสเครื่องเชิงเส้นที่ได้มาจาก WebAssembly ซึ่งจำกัดไว้ในสล็อตหน่วยความจำเชิงเส้น 4 GB ที่ได้รับการป้องกันด้วยฮาร์ดแวร์
  • คีย์การป้องกันหน่วยความจำของ Linux (pku): LFI ใช้คีย์การป้องกันหน่วยความจำของ Linux (pku / pkey_mprotect) เพื่อแบ่งสิทธิ์พื้นที่ที่อยู่ ไลบรารีที่แยกไว้จะอ่านหรือเขียนหน่วยความจำนอกสล็อตแซนด์บ็อกซ์ขนาด 4 GB ที่กำหนดไว้ไม่ได้
  • การดักจับการเรียกใช้ระบบ: ไม่อนุญาตให้มีการเรียกใช้ระบบปฏิบัติการของโฮสต์ (เช่น I/O ของไฟล์ การเข้าถึงเครือข่าย หรือการควบคุมกระบวนการ) ภายในแซนด์บ็อกซ์ ระบบจะกำหนดเส้นทางการเรียกใช้ไลบรารีที่ไม่รองรับไปยัง Stub ดัมมี่ขั้นต่ำ (lfi_stubs.c)
  • การกู้คืนข้อผิดพลาดอย่างปลอดภัย: หากบิตสตรีม Opus ที่มีรูปแบบไม่ถูกต้องหรือเป็นอันตรายพยายามอ่านหรือเขียนนอกขอบเขตภายในแซนด์บ็อกซ์เชิงเส้น รันไทม์ LFI จะดักจับข้อผิดพลาดอย่างปลอดภัยในพื้นที่ผู้ใช้และแสดงข้อผิดพลาดในการถอดรหัสที่สะอาด C2_CORRUPTED โดยไม่ทำให้แอปโฮสต์ขัดข้อง

สถาปัตยกรรมและอุปกรณ์ที่รองรับสำหรับ LFI

LFI ไม่ต้องใช้ชุดคำสั่งฮาร์ดแวร์ CPU ที่กำหนดเองและรองรับสถาปัตยกรรมโปรเซสเซอร์ ARM64 (aarch64) และ x86_64

  • อุปกรณ์ ARM64 (aarch64): อุปกรณ์เคลื่อนที่สำหรับผู้บริโภคส่วนใหญ่ ซึ่งรวมถึงโทรศัพท์มือถือ (เช่น อุปกรณ์ Pixel ที่มี SoC ของ Google Tensor, Qualcomm Snapdragon และ MediaTek SoC), อุปกรณ์พับได้, แท็บเล็ต และหน่วยสาระบันเทิงในรถยนต์ จะทำงานบนโปรเซสเซอร์ ARM64 และรองรับตัวถอดรหัสในกระบวนการ LFI
  • อุปกรณ์ x86_64: โปรแกรมจำลอง Android ที่ทำงานบนเวิร์กสเตชันสำหรับนักพัฒนาซอฟต์แวร์ ที่มี CPU ของ Intel หรือ AMD รวมถึง Chromebook ที่ใช้ ChromeOS ซึ่งเรียกใช้แอป Android โดยใช้ ARC (Android Runtime สำหรับ ChromeOS) จะทำงานบนสถาปัตยกรรม x86_64 และรองรับแซนด์บ็อกซ์ LFI

ตัวถอดรหัสเสียงในกระบวนการที่พร้อมใช้งาน

รูปแบบเสียง ประเภท MIME ชื่อคอมโพเนนต์ในกระบวนการ การใช้งาน
Opus audio/opus c2.android.inproc.opus.decoder C/C++ libopus ที่มี LFI
AAC audio/mp4a-latm c2.android.inproc.aac.decoder Rust ที่ปลอดภัยด้านหน่วยความจำ (C2ApexAacDec)
  • AAC (c2.android.inproc.aac.decoder): จัดการสตรีม ADTS (มีการแยกวิเคราะห์ส่วนหัว ADTS แบบไดนามิก) และหน่วยการเข้าถึง (AU) ดิบที่มีการติดตามการประทับเวลาการนำเสนอที่ถูกต้อง (การประมวลผล AU แบบเฟรมเดียวเท่านั้นในกรณีของ AAC_PACKAGING_RAW)
  • Opus (c2.android.inproc.opus.decoder): ถอดรหัสแพ็กเก็ตคอนเทนเนอร์ Ogg Opus หรือ Matroska มาตรฐานด้วยการสุ่มตัวอย่างภายในเป็น PCM 48 kHz

เลือกใช้ตัวถอดรหัสในกระบวนการ

ปัจจุบันตัวถอดรหัสในกระบวนการไม่ใช่ค่าเริ่มต้นของระบบเมื่อโทรหา MediaCodec.createDecoderByType() แพลตฟอร์มจะยังคงให้ความสำคัญกับการเลือก Codec 2.0 สูงกว่าสำหรับตัวถอดรหัสแบบเดิมที่อยู่นอกกระบวนการในระหว่างการยืนยันระบบนิเวศ

หากต้องการเลือกใช้ตัวถอดรหัสในกระบวนการในวันนี้เพื่อการเล่นที่มีเวลาในการตอบสนองต่ำหรือการทดสอบ ให้สร้างอินสแตนซ์ของตัวแปลงรหัสอย่างชัดเจนตามชื่อคอมโพเนนต์โดยใช้ MediaCodec.createByCodecName()

สร้างอินสแตนซ์ตามชื่อคอมโพเนนต์

ตัวอย่างต่อไปนี้แสดงวิธีสร้างอินสแตนซ์ของตัวถอดรหัส AAC ในกระบวนการอย่างชัดเจน โดยจะกลับไปใช้ตัวถอดรหัสเริ่มต้นหากคอมโพเนนต์ในกระบวนการไม่พร้อมใช้งานในอุปกรณ์

Kotlin

val INPROC_AAC_CODEC = "c2.android.inproc.aac.decoder"

fun createAudioDecoder(): MediaCodec {
    return try {
        // Attempt to opt in to the low-latency in-process AAC decoder
        MediaCodec.createByCodecName(INPROC_AAC_CODEC)
    } catch (e: IllegalArgumentException) {
        // Fall back to the default platform AAC decoder
        MediaCodec.createDecoderByType(MediaFormat.MIMETYPE_AUDIO_AAC)
    }
}

Java

private static final String INPROC_AAC_CODEC = "c2.android.inproc.aac.decoder";

public MediaCodec createAudioDecoder() throws IOException {
    try {
        // Attempt to opt in to the low-latency in-process AAC decoder
        return MediaCodec.createByCodecName(INPROC_AAC_CODEC);
    } catch (IllegalArgumentException e) {
        // Fall back to the default platform AAC decoder
        return MediaCodec.createDecoderByType(MediaFormat.MIMETYPE_AUDIO_AAC);
    }
}

กำหนดค่า Jetpack Media3 หรือ ExoPlayer

หากแอปใช้ Jetpack Media3 หรือ ExoPlayer คุณจะสร้าง MediaCodecSelector ที่กำหนดเองเพื่อจัดลำดับความสำคัญของตัวถอดรหัสในกระบวนการเมื่อพร้อมใช้งานในอุปกรณ์ได้โดยทำดังนี้

Kotlin

class InprocPreferredMediaCodecSelector : MediaCodecSelector {
    override fun getDecoderInfos(
        mimeType: String,
        requiresSecureDecoder: Boolean,
        requiresTunnelingDecoder: Boolean
    ): List<MediaCodecInfo> {
        val defaultInfos = MediaCodecUtil.getDecoderInfos(
            mimeType,
            requiresSecureDecoder,
            requiresTunnelingDecoder
        )
        val inprocNames = setOf(
            "c2.android.inproc.opus.decoder",
            "c2.android.inproc.aac.decoder"
        )
        // Sort in-process decoders to the top of the selection list
        return defaultInfos.sortedByDescending { it.name in inprocNames }
    }
}

Java

public class InprocPreferredMediaCodecSelector implements MediaCodecSelector {
    private static final Set<String> INPROC_NAMES = new HashSet<>(Arrays.asList(
        "c2.android.inproc.opus.decoder",
        "c2.android.inproc.aac.decoder"
    ));

    @Override
    public List<MediaCodecInfo> getDecoderInfos(
            String mimeType,
            boolean requiresSecureDecoder,
            boolean requiresTunnelingDecoder) throws MediaCodecUtil.DecoderQueryException {
        List<MediaCodecInfo> defaultInfos = new ArrayList<>(
            MediaCodecUtil.getDecoderInfos(mimeType, requiresSecureDecoder, requiresTunnelingDecoder)
        );
        // Sort in-process decoders to the top of the selection list
        defaultInfos.sort((a, b) -> {
            boolean aInproc = INPROC_NAMES.contains(a.name);
            boolean bInproc = INPROC_NAMES.contains(b.name);
            return Boolean.compare(bInproc, aInproc);
        });
        return defaultInfos;
    }
}

แผนงานสำหรับตัวถอดรหัสระบบเริ่มต้น

Android กำลังเตรียมพร้อมที่จะเปลี่ยนตัวถอดรหัสในกระบวนการที่ปลอดภัยด้านหน่วยความจำเหล่านี้ ให้เป็นตัวถอดรหัสเริ่มต้นของระบบสำหรับ MIME ของเสียงที่เกี่ยวข้อง ในแพลตฟอร์มรุ่นที่จะเปิดตัว (เริ่มจาก Opus LFI และ AAC Rust ใน Android 18 หรือการอัปเดตโมดูล Mainline ที่กำลังจะมาถึง)

เมื่อตัวถอดรหัสในกระบวนการกลายเป็นคอมโพเนนต์ Codec 2.0 เริ่มต้นสำหรับประเภท MIME ของตัวเอง การเรียกมาตรฐานไปยัง MediaCodec.createDecoderByType(...) จะกำหนดเส้นทางผ่านการติดตั้งใช้งานในกระบวนการโดยอัตโนมัติ ซึ่งจะช่วยลดเวลาในการตอบสนองในการถอดรหัสลงประมาณ 40% โดยไม่ต้องเปลี่ยนแปลงโค้ดแอป