உள்ளடக்க அட்டவணை
- உயர்-நிலை கட்டமைப்பு
- வீடியோ குறியீட்டாக்க குழாய் (Android)
- VideoPacket நெறிமுறை வடிவமைப்பு
- வீடியோ டிகோடிங் குழாய் (Web)
- WebGL2 ரெண்டரிங்
- இழப்பு கண்டறிதல் மற்றும் பிழை மீட்பு
- திரை சுழற்சி மாற்ற கையாளுதல்
- கணினி MediaProjection வாழ்க்கைச் சுழற்சி
- தொலை கட்டுப்பாடு: தொடு உட்செலுத்தல்
- ஆடியோ குழாய்
- செயல்திறன் மேம்படுத்தல்கள்
- வடிவமைப்பு முறைகள் மீள்பார்வை
உயர்-நிலை கட்டமைப்பு
PlainApp ஸ்கிரீன் மிரர் ஒரு முனைக்குமுனை குறைந்த-தாமத ஒளிபரப்பு அமைப்பு ஆகும்: Android சாதனம் திரை உள்ளடக்கத்தை பிடித்து, அதை H.264 வீடியோ மற்றும் Opus ஆடியோவாக வன்பொருள் குறியீடாக்கம் செய்து, தனிப்பயன் பைனரி நெறிமுறையைப் பயன்படுத்தி WebSocket வழியாக Web கிளையண்டுக்கு அனுப்புகிறது; Web கிளையண்ட் WebCodecs API மூலம் டிகோடிங் செய்து WebGL2 மூலம் நேரடியாக Canvas இல் வழங்குகிறது, முழுவதும் CPU பிரதிகள் இல்லாமல்.
WebRTC இல்லை, RTMP இல்லை, இடைநிலை சேவையகம் இல்லை. முழு குழாய் அமைப்பு:
Android VirtualDisplay → MediaCodec H.264 Encoder → WebSocket →
WebCodecs VideoDecoder → WebGL2 Texture → Canvas
ஏன் WebRTC இல்லை?
WebRTC நிகழ்நேர தகவல்தொடர்புக்காக வடிவமைக்கப்பட்டுள்ளது. அதன் ICE/STUN/TURN பேச்சுவார்த்தை, நெரிசல் கட்டுப்பாடு மற்றும் ஜிட்டர் பஃபரிங் ஆகியவை LAN ஸ்கிரீன் காஸ்டிங்கிற்கு அதிகப்படியானவை. PlainApp இன் பயன்பாட்டு சூழ்நிலை:
- ஒரே LAN, தாமதம் < 5ms, NAT ஊடுருவல் தேவையில்லை
- மிகக் குறைந்த தாமதத்தை நாடுகிறது, ஜிட்டர் பஃபரிங் இல்லை
- உயர் தரம், பிட்ரேட் அதிகமாக இருக்கலாம் (8Mbps)
- திரை கட்டுப்பாடு (தொடு உட்செலுத்தல்), WebRTC இன் DataChannel தேவையற்ற சிக்கலைச் சேர்க்கிறது
WebSocket மீதான தனிப்பயன் பைனரி நெறிமுறை LAN சூழ்நிலைக்கு இலகுவானது மற்றும் கட்டுப்படுத்தக்கூடியது.
கூறு வரைபடம்
| அடுக்கு | Android | Web |
|---|---|---|
| திரை பிடிப்பு | MediaProjection + VirtualDisplay | — |
| வீடியோ குறியீட்டாக்கம் | MediaCodec H.264 வன்பொருள் குறியீட்டாக்கி | — |
| ஆடியோ குறியீட்டாக்கம் | MediaCodec Opus வன்பொருள் குறியீட்டாக்கி | — |
| போக்குவரத்து | WebSocket பைனரி நிகழ்வுகள் | WebSocket பெறுநர் |
| வீடியோ டிகோடிங் | — | WebCodecs VideoDecoder |
| ஆடியோ டிகோடிங் | — | WebCodecs AudioDecoder → <audio> |
| வழங்கல் | — | WebGL2 டெக்ஸ்சர் நேரடி-வழங்கல் |
| கட்டுப்பாடு | AccessibilityService சைகை உட்செலுத்தல் | தொடு மேலடுக்கு → GraphQL mutation |
வீடியோ மற்றும் ஆடியோ எப்போதும் சாதனத்திலிருந்து உலாவிக்கு ஒரே WebSocket இணைப்பில் பாய்கிறது; கட்டுப்பாடு எதிர் திசையில் GraphQL (sendScreenMirrorControl) மூலம் பாய்கிறது, இது கோடெக் கட்டமைப்பு (screenMirrorVideoCodec query) மற்றும் கீஃபிரேம் கோரிக்கைகளுக்கான (requestScreenMirrorKeyFrame mutation) பக்க சேனலாகவும் இரட்டிப்பாகிறது.
வீடியோ குறியீட்டாக்க குழாய் (Android)
குறியீட்டாக்க அளவுரு சீரமைப்பு
குறியீட்டாக்க அளவுருக்கள் குறிப்பாக குறைந்த-தாமத LAN ஸ்கிரீன் காஸ்டிங்கிற்காக சீரமைக்கப்பட்டன:
| அளவுரு | மதிப்பு | குறிப்புகள் |
|---|---|---|
KEY_FRAME_RATE | 60 | 60fps மென்மைக்காக |
KEY_I_FRAME_INTERVAL | 10 | IDR இடைவெளி 10வி, கீஃபிரேம் மேல்நிலையை குறைக்கிறது |
KEY_BIT_RATE_MODE | VBR (மறைமுகமானது, வெளிப்படையான முறை அமைக்கப்படவில்லை) | மாறும் பிட்ரேட், காட்சிக்கு ஏற்றது |
KEY_PRIORITY | 0 | நிகழ்நேர முன்னுரிமை |
KEY_LATENCY | 1 | குறைந்த-தாமத முறை |
பிட்ரேட் தர முறைப்படி படிநிலையாக உள்ளது — அதிக பிட்ரேட்டுகள் (எ.கா. 24 Mbps) சோதிக்கப்பட்டன மற்றும் திரை உள்ளடக்கத்திற்கான புலப்படும் தர ஆதாயம் இல்லாமல் குறியீட்டாக்கி/டிகோடர் பிரேம் கைவீச்சு மற்றும் முனைக்குமுனை தாமதத்தை அதிகரித்தன:
| முறை | பிட்ரேட் | பிடிப்பு தெளிவுத்திறன் |
|---|---|---|
| HD | 8 Mbps | 1080p குறுகிய பக்கம் |
| Smooth | 4 Mbps | 1080p குறுகிய பக்கம் |
| Low | 2 Mbps | 720p குறுகிய பக்கம் |
குறியீட்டாக்கி குறைந்த-தாமத கட்டமைப்பு
MediaCodecVideoEncoder உருவாக்கத்தின் போது ஒருமுறை குறியீட்டாக்கியை கட்டமைக்கிறது:
MediaFormat.createVideoFormat(MIME, width, height).apply {
setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface)
setInteger(MediaFormat.KEY_BIT_RATE, bitrateBps)
setInteger(MediaFormat.KEY_FRAME_RATE, frameRate) // 60
setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iFrameIntervalSec) // 10
setLong(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, 100_000L)
setInteger(MediaFormat.KEY_COLOR_RANGE, MediaFormat.COLOR_RANGE_LIMITED)
setInteger(MediaFormat.KEY_PRIORITY, 0)
setInteger(MediaFormat.KEY_LATENCY, 1)
}
KEY_PRIORITY=0 மற்றும் KEY_LATENCY=1 குறைந்த தாமதத்திற்கான திறவுகோல்கள் — அவை குறியீட்டாக்கிக்கு சுருக்க விகிதத்தை விட நிகழ்நேர குறியீட்டாக்கத்திற்கு முன்னுரிமை அளிக்க சொல்கின்றன. உள்ளீடு MediaCodec.createInputSurface() ஆல் உருவாக்கப்பட்ட Surface ஆகும், இது நேரடியாக VirtualDisplay க்கு அளிக்கப்படுகிறது — SurfaceTexture திரும்பப்படித்தல் இல்லை, I420 மாற்றம் இல்லை, CPU பிக்சல்களை தொடாது.
பிடிப்பு தெளிவுத்திறன்
ScreenMirrorCaptureSize.compute() இயற்பியல் திரை அளவு, தர முறையின் குறுகிய-பக்க இலக்கு (720/1080), மற்றும் குறியீட்டாக்கியின் தெரிவித்த maxWidth/maxHeight மற்றும் அகலம்/உயரம் சீரமைப்பு ஆகியவற்றிலிருந்து உண்மையான பிடிப்பு அளவை பெறுகிறது (ஒருமுறை MediaCodecVideoEncoder.queryEncoderCaps() மூலம் வினவப்பட்டது), எனவே குறியீட்டாக்கி ஒருபோதும் ஏற்க முடியாத பரிமாணங்களைப் பெறாது.
கீஃபிரேம் கோரிக்கைகள்
Web கிளையண்ட் GraphQL requestScreenMirrorKeyFrame mutation மூலம் பாக்கெட் இழப்பிலிருந்து மீட்க IDR பிரேமைக் கோரலாம். Android MediaCodec.PARAMETER_KEY_REQUEST_SYNC_FRAME மூலம் பதிலளிக்கிறது:
fun requestKeyFrame() {
val b = Bundle().apply { putInt(MediaCodec.PARAMETER_KEY_REQUEST_SYNC_FRAME, 1) }
codec?.setParameters(b)
}
SPS/PPS மற்றும் கீஃபிரேம் ஒளிபரப்பு
குறியீட்டாக்கி தொடங்கிய பிறகு, INFO_OUTPUT_FORMAT_CHANGED csd-0/csd-1 (SPS/PPS) ஐ வழங்குகிறது, இதை ScreenMirrorPipeline ஒற்றை Annex-B கட்டமைப்பு துண்டாக இணைத்து தற்காலிக சேமிப்பில் வைக்கிறது (cachedConfig). அதைத் தொடர்ந்து வரும் முதல் IDR யும் தற்காலிக சேமிப்பில் வைக்கப்படுகிறது (cachedKeyFrame), இதனால் புதிதாக இணைக்கும் Web கிளையண்ட் அடுத்த கீஃபிரேம் இடைவெளிக்காக காத்திருக்காமல் screenMirrorVideoCodec GraphQL வினவல் மூலம் இரண்டையும் இழுக்க முடியும். கட்டமைப்பு மாறும்போது (திரை சுழற்சி அல்லது தர மாற்றம்), Android புதிய IDR ஐ சாதாரண வீடியோ பாக்கெட்டாக அனுப்புவதில்லை — அது SPS/PPS + IDR ஐ ஒரு screen_mirror_video_codec WebSocket நிகழ்வாக தொகுக்கிறது, இதனால் Web கிளையண்ட் ஒரே அடியில் டிகோடர் மறுகட்டமைப்பு மற்றும் முதல்-பிரேம் டிகோடிங்கை முடிக்கிறது.
சில OEM குறியீட்டாக்கிகள் (Qualcomm/Xiaomi) SPS+PPS+IDR ஐ ஒரே வெளியீடு பஃபரில் தொகுக்கின்றன, இது BUFFER_FLAG_CODEC_CONFIG மற்றும் BUFFER_FLAG_SYNC_FRAME இரண்டையும் கொண்டுள்ளது. வடிகால் சுழற்சி தூய்மையான கட்டமைப்பு (isConfig && !isKey) பஃபர்களை மட்டுமே தவிர்க்கிறது — கட்டமைப்பு கொடியுடன் கூடிய பஃபரைத் தவிர்ப்பது IDR ஐ அமைதியாக கைவிட்டு டிகோடரை P-பிரேம்களுடன் மட்டுமே விட்டுச்செல்லும், மொசைக் வெளியீட்டை உருவாக்கும்.
VideoPacket நெறிமுறை வடிவமைப்பு
வீடியோ மற்றும் ஆடியோ பிரேம்கள் இரண்டும் WebSocket போக்குவரத்திற்காக ஒருங்கிணைந்த VideoPacket பைனரி நெறிமுறையில் பொதிக்கப்பட்டுள்ளன.
நெறிமுறை வடிவம்
+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+
| MAGIC | FLAGS | FRAME_ID (4 bytes, big-endian) | TIMESTAMP (8 bytes, BE) | DATA |
| 0x56 | | byte2 | byte3 | byte4 | byte5 | byte6 | byte7 | ... | byte13 | payload... |
+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+
\- 1B -/ \- 1B -/ \-------------------- 4B ----------------------/ \----------------------- 8B ------------------------/ \- var -/
| புலம் | அளவு | விளக்கம் |
|---|---|---|
MAGIC | 1 பைட் | நிலையான 0x56 ('V'), சரிபார்ப்புக்கு |
FLAGS | 1 பைட் | 0x01=கீஃபிரேம், 0x02=கட்டமைப்பு, 0x04=ஆடியோ |
FRAME_ID | 4 பைட்டுகள் | ஒருபோக்கில் அதிகரிக்கும் பிரேம் எண், uint32 big-endian |
TIMESTAMP | 8 பைட்டுகள் | குறியீட்டாக்கி PTS மைக்ரோசெகண்டுகளில், big-endian |
DATA | மாறும் | H.264 NAL அலகு அல்லது Opus தரவு |
Android இன் VideoPacket.encode() (commonMain இல், எனவே அதன் வயர் வடிவம் JVM யூனிட் சோதனைகளால் எந்த Android சார்புமின்றி மூடப்பட்டுள்ளது) மற்றும் Web இன் parseVideoPacket() இரண்டும் இந்த வடிவமைப்பை சுயாதீனமாக செயல்படுத்துகின்றன — பகிரப்பட்ட தொடர் மாற்ற நூலகம் இல்லை, இரு தரப்பும் மதிக்கும் ஒரு விவரக்குறிப்பு மட்டுமே.
வடிவமைப்பு குறிப்புகள்
FRAME_IDகையொப்பமிடாத பாகுபடுத்தல்:((buf[2] << 24) | (buf[3] << 16) | (buf[4] << 8) | buf[5]) >>> 0— கையொப்பமிடாததை உறுதிப்படுத்த>>> 0ஐப் பயன்படுத்த வேண்டும், இல்லையெனில்frameId > 2^31எதிர்மறையாக பாகுபடுத்தப்பட்டு, தவறான இழப்பு கண்டறிதலை ஏற்படுத்தும்.TIMESTAMPகுறியீட்டாக்கி PTS ஐப் பயன்படுத்துகிறது: Web கிளையண்டின் கடிகாரத்தை சார்ந்திருக்கவில்லை, கடிகார சறுக்கல் A/V ஒத்திசைவின்மையை ஏற்படுத்துவதைத் தவிர்க்கிறது.
வீடியோ டிகோடிங் குழாய் (Web)
WebCodecs VideoDecoder
Web கிளையண்ட் WebCodecs API இன் VideoDecoder ஐ வன்பொருள் டிகோடிங்கிற்கு பயன்படுத்துகிறது. MediaSource Extensions அல்லது WebRTC உடன் ஒப்பிடும்போது, WebCodecs டிகோடிங் செயல்முறையின் மீது நுண்ணிய கட்டுப்பாட்டை வழங்குகிறது — ஜிட்டர் பஃபர் இல்லை, கொள்கலன் அடுக்கு இல்லை, மற்றும் டிகோட் செய்யப்பட்ட VideoFrame பொருள்களை நேரடியாக WebGL டெக்ஸ்சர்களாக பதிவேற்றலாம்.
const decoder = new VideoDecoder({
output: (frame) => this.renderFrame(frame),
error: (e) => {
this.waitingForIdr = true
this.onRequestKeyFrame?.()
this.onError?.(e)
},
})
decoder.configure({
codec, // எ.கா. 'avc1.42c01e', SPS NAL இலிருந்து படிக்கப்பட்டது
avc: { format: 'annexb' },
optimizeForLatency: true,
hardwareAcceleration: 'prefer-hardware',
})
முக்கிய கட்டமைப்புகள்:
optimizeForLatency: true— டிகோடருக்கு குறைந்த தாமதத்திற்கு முன்னுரிமை அளிக்க சொல்கிறது, பிரேம் பஃபரிங் இல்லைhardwareAcceleration: 'prefer-hardware'— GPU டிகோடிங்கை விரும்புகிறதுavc: { format: 'annexb' }— ஒவ்வொரு IDR க்கும் முன் உள்ளமை SPS/PPS உடன் Annex-B வடிவமைப்பைப் பயன்படுத்துகிறது- கோடெக் சரம் கடினக்குறியீடு செய்யப்படவில்லை —
extractAvc1CodecString()கட்டமைப்பு துண்டில் உள்ள முதல் SPS NAL இலிருந்து நேரடியாக profile/compat/level பைட்டுகளைப் படிக்கிறது
பச்சை திரை பிரச்சனை மற்றும் தொடக்க வரிசை
குறியீட்டாக்கி VirtualDisplay உண்மையான திரை உள்ளடக்கத்தை வழங்குவதற்கு முன்பே அதன் முதல் IDR பிரேமை உருவாக்குகிறது — இது ஒரு வெற்று (பச்சை) பிரேம். Web பக்கம் இந்த பிரேமை டிகோட் செய்தால், திரை உள்ளடக்கம் மாறி புதிய பிரேமை தூண்டும் வரை பயனர் ஒரு பச்சை ஒளிர்வைப் பார்ப்பார்.
தீர்வு: தொடக்கத்தில், Web பக்கம் screenMirrorVideoCodec GraphQL வினவல் மூலம் தற்காலிக சேமிப்பில் உள்ள கட்டமைப்பை இழுக்கிறது, ஆனால் தொகுக்கப்பட்ட கீஃபிரேமை டிகோட் செய்யாது. அதற்கு பதிலாக, அது video.requestIdr() ஐ அழைத்து waitingForIdr = true ஐ அமைக்கிறது (IDR வரும் வரை அனைத்து P-பிரேம்களையும் கைவிடுகிறது), பின்னர் இழப்பு மீட்புக்கு பயன்படுத்தப்படும் அதே mutation மூலம் புதிய IDR ஐக் கோர requestKeyFrame() ஐ அழைக்கிறது. புதிய IDR வரும்போது, VirtualDisplay இல் உண்மையான திரை உள்ளடக்கம் இருக்கும்.
video.requestIdr() // P-பிரேம்களை கைவிடு, IDR க்காக காத்திரு
await requestKeyFrame() // GraphQL mutation மூலம் புதிய IDR ஐக் கோரிக்கை
onFirstFrameRendered கால்பேக் handleVideo() ஐ விட renderFrame() உடன் பிணைக்கப்பட்டுள்ளது, UI உண்மையான பிரேம் வழங்கப்பட்ட பின்னரே புதுப்பிக்கப்படுவதை உறுதி செய்கிறது — வெறுமனே பெறப்பட்டதும் அல்ல.
WebGL2 ரெண்டரிங்
பூஜ்ஜிய-நகல் GPU நேரடி வழங்கல்
டிகோட் செய்யப்பட்ட VideoFrame பொருள்கள் நேரடியாக WebGL2 டெக்ஸ்சர்களாக பதிவேற்றப்படுகின்றன, ஒருபோதும் CPU வழியாக செல்லாமல்:
VideoDecoder → VideoFrame → gl.texImage2D(VideoFrame) → Canvas
gl.texImage2D VideoFrame ஐ பிக்சல் மூலமாக ஏற்கிறது. உலாவி உள்ளிருப்பாக YUV→RGB மாற்றம் மற்றும் GPU பதிவேற்றத்தை கையாளுகிறது — ImageData CPU நகல் இல்லை. getContext('webgl2', ...) தோல்வியுற்றால் MirrorGLRenderer Canvas 2D drawImage() க்கு மாறுகிறது, எனவே பழைய உலாவிகளும் (சற்று அதிக-தாமதம் கொண்ட) படத்தைப் பெறுகின்றன.
desynchronized சூழல்
const gl = canvas.getContext('webgl2', {
alpha: false,
desynchronized: true, // கம்போசிட்டரைத் தவிர்த்து, நேரடியாக திரைக்கு எழுது
preserveDrawingBuffer: true, // ஸ்கிரீன்ஷாட்டுகளுக்கு பஃபரைப் பாதுகாக்கவும்
powerPreference: 'high-performance',
antialias: false,
depth: false,
stencil: false,
premultipliedAlpha: false,
})
desynchronized: true உலாவி கம்போசிட்டரைத் தவிர்த்து, நேரடியாக திரைக்கு எழுதுகிறது, சுமார் 1 பிரேம் காட்சி தாமதத்தை (~16ms @ 60fps) மிச்சப்படுத்துகிறது.
preserveDrawingBuffer: true வரைதல் பஃபரைப் பாதுகாக்கிறது, இதனால் canvas.toDataURL() ஸ்கிரீன்ஷாட்கள் உள்ளடக்கத்தைப் படிக்க முடியும். இயல்புநிலை false உடன், கம்போசிட்டிங் பிறகு பஃபர் அழிக்கப்பட்டு, கருப்பு ஸ்கிரீன்ஷாட்களை உருவாக்குகிறது.
ஷேடரே வேண்டுமென்றே மிகக் குறைவாக உள்ளது — ஒரு முழுத்திரை-முக்கோண வெர்டெக்ஸ் ஷேடர் மற்றும் டெக்ஸ்சரை மாதிரி எடுக்கும் ஒரு வரி பிராக்மென்ட் ஷேடர் — ஏனெனில் ஒரு பிரேமுக்கு தேவையான ஒரே வேலை "இந்த டெக்ஸ்சரை திரையில் வை" என்பதுதான்.
Canvas தானியங்கி பொருத்துதல்
Canvas இன் பேக்கிங் ஸ்டோர் அளவு VideoFrame.displayWidth/Height இலிருந்து அது மாறும் போதெல்லாம் அமைக்கப்படுகிறது. CSS அளவு பின்னர் fitCanvasToWrapper() மூலம் உறை கொள்கலனுக்கு பொருத்தப்படுகிறது, அதே நேரத்தில் அம்ச விகிதத்தைப் பாதுகாக்கிறது (தேவைக்கேற்ப லெட்டர்பாக்சிங் அல்லது பில்லர்பாக்சிங்). Canvas இன் பெற்றோர் உறுப்பில் உள்ள ResizeObserver கொள்கலன் மறுஅளவிடும்போதெல்லாம் இந்த பொருத்தத்தை மீண்டும் இயக்குகிறது, எனவே வீடியோ ஒருபோதும் நீட்சி அடையாது.
இழப்பு கண்டறிதல் மற்றும் பிழை மீட்பு
FrameId இடைவெளி கண்டறிதல்
ஒவ்வொரு வீடியோ பிரேமும் ஒருபோக்கில் அதிகரிக்கும் frameId ஐ எடுத்துச் செல்கிறது. டிகோடர் lastFrameId ஐ கண்காணிக்கிறது; ஒரு புதிய பிரேமின் frameId > lastFrameId + 1 எனில், பிரேம்கள் இழக்கப்பட்டன:
if (!this.waitingForIdr && this.lastFrameId > 0
&& packet.frameId > this.lastFrameId + 1) {
if (!packet.isKeyFrame) {
// இழப்பு: அடுத்தடுத்த P-பிரேம்களை கைவிடு, புதிய IDR ஐக் கோரிக்கை
this.waitingForIdr = true
this.onRequestKeyFrame?.()
this.lastFrameId = packet.frameId
return
}
}
waitingForIdr நிலை இயந்திரம்
waitingForIdr ஒரு எளிய இரண்டு-நிலை இயந்திரம்:
| நிலை | நடத்தை |
|---|---|
NORMAL | அனைத்து பிரேம்களையும் சாதாரணமாக டிகோட் செய் |
WAITING_FOR_IDR | அனைத்து P-பிரேம்களையும் கைவிடு, IDR பிரேம்களை மட்டும் டிகோட் செய்; IDR வரும்போது NORMAL க்கு மீட்டமை |
WAITING_FOR_IDR க்கு மாற்றத்தை தூண்டும் சூழ்நிலைகள்:
- தொடக்கத்தில்: பழைய GraphQL கீஃபிரேமைத் தவிர்த்து, உண்மையான IDR க்காக காத்திரு
- பாக்கெட் இழப்பு ஏற்படும்போது: டிகோட் செய்ய முடியாத P-பிரேம்களை கைவிடு, IDR மீட்புக்காக காத்திரு
- டிகோடர் பிழை ஏற்படும்போது: டிகோடரை மீட்டமை, IDR க்காக காத்திரு
- கட்டமைப்பு மாற்றம் ஏற்படும்போது: திரை சுழற்சி/தர மாற்றத்திற்குப் பிறகு எஞ்சிய P-பிரேம்களை கைவிடு
டிகோடர் பிழை மீட்பு
VideoDecoder.onerror செயல்படும்போது, குழாய் அடுக்கில் (screen-mirror-pipeline.ts) decoderNeedsReset = true அமைக்கப்படுகிறது. அடுத்த IDR பிரேமில், மீண்டும் GraphQL க்கு சுற்றுப்பயணம் செய்வதற்கு பதிலாக தற்காலிக சேமிப்பில் உள்ள SPS/PPS உடன் டிகோடர் மறுகட்டமைக்கப்படுகிறது:
if (decoderNeedsReset) {
if (!packet.isKeyFrame || !cachedConfig) return
video.configure(cachedConfig)
decoderNeedsReset = false
}
பின்னழுத்தம் மற்றும் நேரமுத்திரை நகல்நீக்கம்
decoder.decodeQueueSize > 5 எனில், உள்வரும் P-பிரேம்கள் வரிசையில் வைப்பதற்கு பதிலாக கைவிடப்படுகின்றன — 5 இன் வரம்பு (2 க்கு பதிலாக) வன்பொருள் டிகோடர் தொடக்க தாமதத்தை தேவையற்ற தடுமாற்றத்தை ஏற்படுத்தாமல் பொறுத்துக்கொள்கிறது. தனித்தனியாக, வழங்கிய பிறகு, lastRenderedPts பதிவு செய்யப்படுகிறது; நேரமுத்திரை பழையதாக இருந்தால் (வரிசை தவறிய வருகை) அது கீஃபிரேம் இல்லையெனில் கைவிடப்படுகிறது:
if (packet.timestamp < this.lastRenderedPts && !packet.isKeyFrame) {
return
}
திரை சுழற்சி மாற்ற கையாளுதல்
குறியீட்டாக்கி மறுகட்டமைப்பு
ScreenMirrorService இல் உள்ள OrientationEventListener ஒவ்வொரு சென்சார் கால்பேக்கிலும் காட்சியின் rotation ஐ தற்காலிக சேமிப்பில் உள்ள isPortrait கொடியுடன் ஒப்பிடுகிறது; உண்மையான portrait/landscape மாற்றம் மட்டுமே pipeline.onOrientationChanged() ஐ அழைத்து தொடு ஒருங்கிணைப்பு அளவிடலுக்கு பயன்படுத்தப்படும் அணுகல்தன்மை திரை-அளவு தற்காலிக சேமிப்பை செல்லாததாக்குகிறது.
rebuildEncoderAndResize():
- புதிய பரிமாணங்களில் புதிய குறியீட்டாக்கியை உருவாக்கு (எ.கா. landscape 1920x1080)
VirtualDisplay.surfaceஐ புதிய குறியீட்டாக்கியின் உள்ளீடுSurfaceக்கு மாற்று- பழைய குறியீட்டாக்கியை நிறுத்து
VirtualDisplay.resize()ஐ புதிய பரிமாணங்களுக்கு
Surface மாற்றம் resize க்கு முன் நடைபெறுகிறது — புதிய குறியீட்டாக்கி முதலில் பிரேம்களைப் பெறுவதை உறுதி செய்கிறது, மற்றும் பழைய குறியீட்டாக்கி தவறான பரிமாண பிரேம்களைப் பெறுவதற்கு முன் நிறுத்தப்படுகிறது. virtualDisplay?.surface = ... விதிவிலக்கு எறிந்தால், மறுகட்டமைப்பு கைவிடப்பட்டு பழைய குறியீட்டாக்கி இயங்க வைக்கப்படுகிறது, குழாயை குறியீட்டாக்கி இல்லாமல் விட்டுவிடாமல்.
கட்டமைப்பு மாற்ற அறிவிப்பு
புதிய குறியீட்டாக்கி முதலில் SPS/PPS ஐ வெளியிடும்போது, குழாயில் pendingConfigBroadcast அமைக்கப்படுகிறது. புதிய குறியீட்டாக்கியிலிருந்து முதல் IDR வரும்போது, அது அந்த கட்டமைப்புடன் ஒரு சாதாரண வீடியோ பாக்கெட்டாக அனுப்பப்படுவதற்கு பதிலாக ஒற்றை screen_mirror_video_codec நிகழ்வாக தொகுக்கப்படுகிறது.
Web கிளையண்ட் பின்னர், handleConfig() இல்:
- புதிய SPS/PPS உடன் டிகோடரை மறுகட்டமைக்கிறது
- தொகுக்கப்பட்ட IDR பிரேமை உடனடியாக டிகோட் செய்கிறது
- பழைய குறியீட்டாக்கியிலிருந்து இன்னும் பறந்து கொண்டிருக்கும் எஞ்சிய P-பிரேம்களை கைவிட
video.requestIdr()ஐ அழைக்கிறது - சுத்தமான, புதிய IDR ஐக் கோர
requestKeyFrame()ஐ அழைக்கிறது
படிகள் 3-4 ஒரு பாதுகாப்பு வலை — புதிய குறியீட்டாக்கியின் முதல் IDR தவறான பரிமாணங்களைக் கொண்டிருந்தாலும் (async resize சாளரத்தின் போது), Web கிளையண்ட் விரைவாக சரியான பரிமாணங்களுக்கு மீள்கிறது. handleConfig() உள்வரும் கட்டமைப்பு தற்காலிக சேமிப்பில் உள்ளதற்கு பைட்-ஒத்ததாக இருந்தால் குறுக்குவழி செய்கிறது, ஏனெனில் மாறாத பைட்டுகளுடன் டிகோடரை மறுகட்டமைப்பது ஒரு செயலற்ற செயலாகும், அதிலிருந்து மீள ஒரு IDR செலவாகும்.
கணினி MediaProjection வாழ்க்கைச் சுழற்சி
பிரச்சனை
பயனர்கள் பயன்பாட்டின் UI வழியாக அல்ல, Android கணினி அறிவிப்புப் பட்டியின் மூலம் கணினி-நிலை ஸ்கிரீன் காஸ்ட்டை (MediaProjection) மூடலாம். இந்த வழக்கில், ScreenMirrorService காஸ்டிங் நிறுத்தப்பட்டதை அறியாது — running true ஆகவே இருக்கும், Web கிளையண்ட் screenMirrorState ஐ வினவி true ஐப் பெறுகிறது, ஆனால் எந்த வீடியோ பிரேம்களும் வராது, மற்றும் பக்கம் லோடிங்கில் சிக்கிக் கொள்கிறது.
MediaProjection.Callback
MediaProjection ஒரு Callback.onStop() கால்பேக்கை வழங்குகிறது, இது கணினி காஸ்டிங் நிறுத்தப்படும் போது செயல்படுகிறது. ScreenMirrorPipeline.startEncoders() இந்த கால்பேக்கை பதிவு செய்து onStop() இல் ScreenMirrorService.instance?.stop() ஐ அழைக்கிறது:
projection.registerCallback(object : MediaProjection.Callback() {
override fun onStop() {
ScreenMirrorService.instance?.stop()
}
}, null)
Service.stop() இன் பொறுப்புகள்
stop() என்பது வெளிப்படையான நிறுத்த புள்ளியாகும், இது Web கிளையண்டுக்கு அறிவிப்பதற்கும் சேவையை நிறுத்துவதற்கும் பொறுப்பானது:
fun stop() {
if (!running) return // மறுநிகழ்வைத் தடுக்க
running = false
sendEvent(WebSocketEvent(EventType.SCREEN_MIRRORING, """{"running":false}"""))
stopForeground(STOP_FOREGROUND_REMOVE)
stopSelf()
}
if (!running) return காவல் மறுநிகழ்வைத் தடுக்கிறது: onStop() → stop() → stopSelf() → onDestroy() → pipeline.stop() → projection.stop() → onStop() → stop() (இந்த கட்டத்தில் running=false, உடனடியாக திரும்புகிறது).
Web-பக்கம் கையாளுதல்
Web கிளையண்ட் {"running":false} நிகழ்வைப் பெறும்போது, அது செயலற்ற நிலைக்கு மீட்டமைக்கப்பட்டு தொடக்க பொத்தானைக் காட்டுகிறது:
const onScreenMirroring = (data: any) => {
if (data?.running === false) {
cleanupFn()
fullReset()
return
}
// running=true → ஸ்ட்ரீமுடன் இணைக்கவும்
}
தொலை கட்டுப்பாடு: தொடு உட்செலுத்தல்
ஸ்கிரீன் மிரரிங் இயல்புநிலையாக ஒரு-வழி (வீடியோ/ஆடியோ மட்டும்); தொலை கட்டுப்பாடு விருப்பத்தேர்வு மற்றும் பயனர் PlainApp இன் Accessibility Service ஐ ஒருமுறை இயக்க வேண்டும், ஏனெனில் Android AccessibilityService.dispatchGesture() க்கு வெளியே தன்னிச்சையான தொடு நிகழ்வுகளை உட்செலுத்த பொது API இல்லை.
ஒருங்கிணைப்பு இயல்பாக்கம் (Web)
<canvas> க்கு மேலே ஒரு வெளிப்படையான மேலடுக்கு அமர்ந்து பாயிண்டர் நிகழ்வுகளைப் பிடிக்கிறது. normalizeCoords() மூல clientX/clientY ஐ உண்மையான வீடியோ உள்ளடக்க பகுதியுடன் தொடர்புடைய [0,1] ஒருங்கிணைப்புகளாக மாற்றுகிறது — மேலடுக்கின் எல்லை பெட்டி அல்ல — Canvas இன் பேக்கிங்-ஸ்டோர் அம்ச விகிதம் அதன் வழங்கப்பட்ட கொள்கலன் அம்ச விகிதத்துடன் ஒப்பிடுவதன் மூலம் லெட்டர்பாக்ஸ்/பில்லர்பாக்ஸ் ஆஃப்செட்டைக் கணக்கிடுகிறது:
if (videoAspect > containerAspect) {
// மேல்/கீழ் லெட்டர்பாக்ஸ்
renderW = containerW
renderH = containerW / videoAspect
offsetY = (containerH - renderH) / 2
} else {
// இடது/வலது பில்லர்பாக்ஸ்
renderH = containerH
renderW = containerH * videoAspect
offsetX = (containerW - renderW) / 2
}
ஒரு பாயிண்டர் அழுத்தம் தொடக்க நிலை/நேரத்தை கண்காணிக்கும் GestureState ஐ தொடங்குகிறது; 10px க்கும் குறைவான இயக்கத்துடன் 500ms நீடிப்பு LONG_PRESS ஆக உயர்கிறது, அந்த வரம்பை தாண்டிய இயக்கம் SWIPE ஆகிறது, மற்றும் விரைவான விடுவிப்பு TAP ஆகும். ஒரு காட்சி தொடு காட்டி (வளர்ந்து மங்கும் புள்ளி) ஆபரேட்டருக்கு என்ன சைகை அங்கீகரிக்கப்பட்டது என்பதற்கான பின்னூட்டத்தை அளிக்கிறது, தொலைபேசி பதிலளிப்பதற்கு முன்பே.
GraphQL → AccessibilityService
ஒவ்வொரு அங்கீகரிக்கப்பட்ட சைகையும் ஒரு sendScreenMirrorControl(input) mutation ஆக அனுப்பப்படுகிறது, இது ஒரு action (TAP/LONG_PRESS/SWIPE/SCROLL/BACK/HOME/RECENTS/LOCK_SCREEN/KEY) மற்றும் இயல்பாக்கப்பட்ட ஒருங்கிணைப்புகளை எடுத்துச் செல்கிறது. தீர்வி dispatchScreenMirrorControl() ஐ அழைக்கிறது, இது இயல்பாக்கப்பட்ட ஒருங்கிணைப்புகளை உண்மையான திரை அளவால் பெருக்குகிறது (PlainAccessibilityService.getScreenSize() இலிருந்து, ஒவ்வொரு திரை சுழற்சி மாற்றத்திலும் செல்லாததாக்கப்படுகிறது) மற்றும் PlainAccessibilityService.dispatchControl() க்கு ஒப்படைக்கிறது:
private fun dispatchTap(x: Float, y: Float) {
val path = Path().apply { moveTo(x, y) }
val stroke = GestureDescription.StrokeDescription(path, 0, 50)
dispatchGesture(GestureDescription.Builder().addStroke(stroke).build(), null, null)
}
SWIPE மற்றும் LONG_PRESS ஆகியவை நீண்ட பக்கவாத கால அளவு அல்லது ஒற்றை புள்ளிக்கு பதிலாக ஒரு கோடு பாதையுடன் அதே GestureDescription ஐ உருவாக்குகின்றன; SCROLL என்பது (x, y) இலிருந்து (x, y + deltaY) வரை ±500px க்கு கட்டுப்படுத்தப்பட்ட ஒரு செயற்கை ஸ்வைப்பாக செயல்படுத்தப்படுகிறது. நான்கு உலகளாவிய செயல்கள் (BACK/HOME/RECENTS/LOCK_SCREEN) சைகை அனுப்புதலை முற்றிலும் தவிர்த்து நேரடியாக performGlobalAction() ஐ அழைக்கின்றன. Accessibility Service இயக்கப்படவில்லை என்றால், தீர்வி உள்ளீட்டை அமைதியாக கைவிடுவதற்கு பதிலாக GraphQLError ஐ எறிகிறது, எனவே Web UI பயனரை அதை இயக்கும்படி கேட்க முடியும்.
ஆடியோ குழாய்
Android Opus குறியீட்டாக்கம்
MediaCodecAudioEncoder AudioRecord மூலம் கணினி ஆடியோவைப் பிடிக்க AudioPlaybackCaptureConfiguration (அதே MediaProjection இலிருந்து கட்டமைக்கப்பட்டது) ஐப் பயன்படுத்துகிறது, மூல PCM ஐ MediaCodec Opus குறியீட்டாக்கிக்கு அளிக்கிறது. இதற்கு Android 10+ மற்றும் RECORD_AUDIO அனுமதி தேவை — பழைய சாதனங்களில் அல்லது அனுமதி இல்லாமல், start() ஒரு எச்சரிக்கையைப் பதிவு செய்து ஆடியோவை முற்றிலும் தவிர்க்கிறது (வீடியோ தொடர்ந்து வேலை செய்கிறது). குறியீடாக்கப்பட்ட Opus பாக்கெட்டுகள் அதே VideoPacket நெறிமுறையில் (FLAG_AUDIO அமைக்கப்பட்டு) பொதிக்கப்பட்டு வீடியோ பாக்கெட்டின் SCREEN_MIRROR_AUDIO WebSocket சேனலைப் பகிர்ந்து கொள்கின்றன.
Web Opus டிகோடிங்
ScreenMirrorAudioPipeline Opus தரவை டிகோட் செய்ய WebCodecs AudioDecoder ஐப் பயன்படுத்துகிறது, <audio> உறுப்புக்கு அனுப்பப்படும் AudioData வெளியீடாக. ஆடியோ பிரேம் timestamp A/V ஒத்திசைவுக்கு பயன்படுத்தப்படுகிறது — வீடியோ பிரேம்களுடன் அதே நேர அடிப்படையை (குறியீட்டாக்கி PTS, மைக்ரோசெகண்டுகளில்) பகிர்ந்து கொள்கிறது, எனவே இரண்டு ஸ்ட்ரீம்களுக்கும் இடையே தனி கடிகார பேச்சுவார்த்தை தேவையில்லை.
செயல்திறன் மேம்படுத்தல்கள்
பூஜ்ஜிய-நகல் பாதைகள்
| பாதை | முறை |
|---|---|
| VirtualDisplay → குறியீட்டாக்கி Surface | GPU நேரடி, Surface நேரடி அனுப்புதல் |
| VideoDecoder → VideoFrame → WebGL டெக்ஸ்சர் | gl.texImage2D(VideoFrame), GPU நேரடி |
| WebSocket பெறுதல் → VideoPacket பாகுபடுத்தல் | Uint8Array.subarray() ஒரு பார்வை, நகல் இல்லை |
avccToAnnexB மேம்படுத்தல்
சில Android குறியீட்டாக்கிகள் AVCC வடிவமைப்பை (4-பைட் நீள முன்னொட்டு) வெளியிடுகின்றன, இது WebCodecs டிகோடிங்கிற்கு Annex-B வடிவமைப்புக்கு (00 00 00 01 தொடக்க குறியீடு) மாற்றப்பட வேண்டும்.
ஆரம்ப செயலாக்கம் ArrayList<Byte> ஐ ஒரு பைட்டுக்கு ஒரு பெட்டியாக்கத்துடன் பயன்படுத்தியது — 50KB IDR பிரேம் 50,000 java.lang.Byte பெட்டியாக்க செயல்பாடுகளை உருவாக்கியது, பாரிய GC அழுத்தத்தை உருவாக்கியது. மேம்படுத்தல் இரண்டு-பாஸ் ஸ்கேன் + copyInto (JVM இல் System.arraycopy intrinsic க்கு மேப்பிங் செய்கிறது) பயன்படுத்துகிறது:
// முதல் பாஸ்: வெளியீடு அளவை கணக்கிடு
var outSize = 0
// இரண்டாம் பாஸ்: மொத்த நகல்
val out = ByteArray(outSize)
avcc.copyInto(out, writeOff + 4, off + 4, off + 4 + len)
P-பிரேம் கைவீச்சு உத்தி
டிகோடர் தொடக்கத்தின் போது மெதுவாக இருக்கலாம். P-பிரேம் வரிசை மிக நீளமாக இருந்தால், தாமதம் குவிகிறது. டிகோட் வரிசை அளவு வரம்பு > 5 ( > 2 க்கு பதிலாக) அமைக்கப்பட்டுள்ளது, வன்பொருள் டிகோடர் தொடக்கத்தின் போது அதிகப்படியான பிரேம் இழப்பைத் தவிர்க்க.
IDR கோரிக்கை நகல்நீக்கம்
waitingForIdr காவல் ஒவ்வொரு இழப்பு நிகழ்வுக்கும் ஒரே ஒரு IDR கோரிக்கை மட்டுமே இருப்பதை உறுதி செய்கிறது, IDR வரும் வரை காத்திருக்கும் போது நகல் கோரிக்கைகளைத் தடுக்கிறது.
வடிவமைப்பு முறைகள் மீள்பார்வை
| முறை | எங்கு | ஏன் |
|---|---|---|
| நிலை இயந்திரம் | waitingForIdr கொடி | வெளிப்படையான P-பிரேம் கைவீச்சு/மீட்பு நிலை மாற்றங்கள் |
| மறுநிகழ்வு காவல் | stop() இல் if (!running) return | onStop → stop → onDestroy → pipeline.stop → projection.stop → onStop மறுநிகழ்வைத் தடுக்கிறது |
| பூஜ்ஜிய-நகல் குழாய் | VideoFrame → gl.texImage2D | GPU நேரடி டெக்ஸ்சர் பதிவேற்றம், CPU நகல் இல்லை |
| இரண்டு-பாஸ் ஸ்கேன் | avccToAnnexB | முன்-கணக்கீடு அளவு, ஒற்றை ஒதுக்கீடு + மொத்த நகல், பெட்டியாக்கத்தை நீக்குகிறது |
| தொகுக்கப்பட்ட நிகழ்வு | SPS/PPS + IDR ஒரு நிகழ்வில் | கட்டமைப்பு மாற்றம் ஒரு நிகழ்வில் மறுகட்டமைப்பு + முதல்-பிரேம் டிகோடிங்கை நிறைவு செய்கிறது |
| கால்பேக் பிரிப்பு | onFirstFrameRendered vs onDisconnected vs onScreenMirrorOff | முதல்-பிரேம் வழங்கல், போக்குவரத்து தோல்வி, மற்றும் தொலைபேசி-பக்கம் நிறுத்தம் ஆகியவற்றுக்கு இடையே தெளிவான வேறுபாடு |
| பாதுகாப்பு வலை | requestIdr() + requestKeyFrame() | கட்டமைப்பு மாற்றத்திற்குப் பிறகு எஞ்சிய பிரேம்களை கைவிடு + சுத்தமான IDR ஐக் கோரிக்கை |
| PTS நகல்நீக்கம் | timestamp < lastRenderedPts | வரிசை தவறிய பிரேம்களை கைவிடு |
| FrameId இடைவெளி | frameId > lastFrameId + 1 | ACK இல்லாத பாக்கெட் இழப்பு கண்டறிதல் |
| desynchronized சூழல் | WebGL2 desynchronized: true | கம்போசிட்டரைத் தவிர்த்து, 1 பிரேம் தாமதத்தை மிச்சப்படுத்து |
| வெளிப்படையான வேக தோல்வி | sendScreenMirrorControl GraphQLError ஐ எறிகிறது | "அணுகல்தன்மை முடக்கப்பட்டது" என்பதை மேற்பரப்புக்கு கொண்டு வருகிறது, உள்ளீட்டை அமைதியாக கைவிடாமல் |
மேலும் படிக்க
- WebCodecs API —
VideoDecoder/AudioDecoderஇடைமுகங்களை உள்ளடக்கிய MDN ஆவணம்.