Media3 cung cấp các cấu phần phần mềm kiểm thử chuyên dụng (media3-test-utils và media3-test-utils-robolectric) để giúp bạn kiểm thử quá trình phát nội dung nghe nhìn, chuyển đổi trạng thái trình phát, thành phần giao diện người dùng và nguồn nội dung nghe nhìn tuỳ chỉnh trong cả kiểm thử JVM cục bộ và kiểm thử đo lường thiết bị.
Trang này trình bày các phương pháp hay nhất được đề xuất để kiểm thử ứng dụng sử dụng ExoPlayer và cho biết cách sử dụng các tiện ích kiểm thử của Media3 trong từng môi trường thử nghiệm.
Đề xuất thử nghiệm
Khi viết kiểm thử tự động cho hoạt động phát nội dung nghe nhìn, hãy tuân theo các nguyên tắc cốt lõi sau:
- Ưu tiên các thành phần thực hoặc thành phần giả lập Media3 hơn là mô phỏng: Tránh mô phỏng
PlayerhoặcExoPlayerbằng các khung như Mockito.Playercó các bất biến trạng thái và hợp đồng trình nghe mà mô phỏng không thực thi, điều này có thể khiến các kiểm thử trở nên không ổn định hoặc ẩn lỗi. Thay vào đó, hãy sử dụng một thực thểExoPlayerthực,FakePlayerhoặc các bản mô phỏng nhưFakeMediaSourcevàFakeDataSource. Dành riêng mô phỏng để xác minh lệnh gọi lại tuỳ chỉnh khi cần. - Chờ một cách tất định thay vì ở trạng thái ngủ:
ExoPlayerthực hiện việc tải và phát nội dung nghe nhìn không đồng bộ trên một luồng nội bộ. Tránh các lệnh gọiThread.sleeptuỳ ý vì chúng khiến các kiểm thử bị chậm và không ổn định. Sử dụngTestPlayerRunHelpertrong các kiểm thử Robolectric và tạm ngưng các tiện íchPlayerhoặcPlayerFencetrong các kiểm thử đo lường. Các trình trợ giúp này chờ các điều kiện rõ ràng của trình phát và không thành công ngay lập tức nếu xảy ra lỗi phát. - Truy cập vào trình phát trên luồng ứng dụng của trình phát: Bạn phải gọi các thực thể
ExoPlayervà trình trợ giúp chờ trên luồngLoopercủa ứng dụng trình phát (thường là luồng chính). Để biết thêm thông tin, hãy xem các yêu cầu về luồng của ExoPlayer.
Chọn môi trường thử nghiệm phù hợp
Các lớp khác nhau trong quá trình tích hợp nội dung nghe nhìn sẽ được hưởng lợi từ các môi trường thử nghiệm khác nhau:
| Mục tiêu thử nghiệm | Môi trường đề xuất | Các tiện ích chính của Media3 |
|---|---|---|
Thành phần giao diện người dùng (PlayerView, Giao diện người dùng Compose), ViewModel hoặc lệnh gọi lại MediaSession |
Kiểm thử Robolectric cục bộ (src/test) hoặc kiểm thử giao diện người dùng đo lường (src/androidTest) |
FakePlayer |
Tích hợp ExoPlayer, chuyển đổi danh sách phát, số liệu phân tích và logic MediaSource hoặc DataSource tuỳ chỉnh |
Kiểm thử Robolectric cục bộ (src/test) |
TestExoPlayerBuilder, TestPlayerRunHelper, FakeMediaSource |
Giải mã MediaCodec, kết xuất Surface, đầu ra âm thanh và DRM phần cứng thực tế |
Kiểm thử đo lường trên thiết bị thực (src/androidTest) |
ExoPlayer.Builder, Player.await* tiện ích, PlayerFence |
Nếu có thể, hãy chạy kiểm thử phát lại đo lường trên thiết bị thực thay vì trình mô phỏng, vì việc triển khai ngăn xếp nội dung nghe nhìn của trình mô phỏng có thể khác nhau. Để biết thêm thông tin về việc hỗ trợ trình mô phỏng, hãy xem hướng dẫn về thiết bị được hỗ trợ.
Thêm các phần phụ thuộc kiểm thử
Thêm cấu phần phần mềm kiểm thử Media3 vào khối dependencies của tệp build.gradle.kts hoặc build.gradle trong mô-đun bằng cách sử dụng cùng phiên bản với các mô-đun Media3 khác:
Kotlin
testImplementation("androidx.media3:media3-test-utils:1.11.1")
testImplementation("androidx.media3:media3-test-utils-robolectric:1.11.1")
androidTestImplementation("androidx.media3:media3-test-utils:1.11.1")
Groovy
testImplementation "androidx.media3:media3-test-utils:1.11.1"
testImplementation "androidx.media3:media3-test-utils-robolectric:1.11.1"
androidTestImplementation "androidx.media3:media3-test-utils:1.11.1"
Hai mô-đun này phân chia trách nhiệm theo môi trường thời gian chạy:
media3-test-utils: Các tiện ích đa môi trường cho cả kiểm thử cục bộ và kiểm thử đo lường, bao gồmTestExoPlayerBuilder,PlayerFence(và các tiện ích KotlinPlayer.await*),FakePlayer,FakeClock,FakeMediaSource,FakeDataSource,DataSourceContractTestvàMedia3FlagsRule.media3-test-utils-robolectric: Các tiện ích cho chương trình kiểm thử Robolectric trên JVM cục bộ, bao gồmTestPlayerRunHelper,RobolectricUtil,ShadowMediaCodecConfig,CapturingRenderersFactoryvàPlaybackOutput.
Đảm bảo mô-đun của bạn cũng bao gồm các phần phụ thuộc khung kiểm thử cho môi trường thử nghiệm:
- Robolectric:
org.robolectric:robolectric. Đặtandroid.testOptions.unitTests.isIncludeAndroidResources = truekhi tải thành phần cục bộ. - Kiểm thử AndroidX:
androidx.test:runner,androidx.test:corevàandroidx.test.ext:junit. - Coroutine của Kotlin trên luồng chính:
org.jetbrains.kotlinx:kotlinx-coroutines-android.
Chọn sử dụng các API kiểm thử không ổn định
Các lớp và phương thức trong media3-test-utils và media3-test-utils-robolectric được chú thích bằng @UnstableApi. Mặc dù Media3 hướng đến việc duy trì các trình trợ giúp kiểm thử được sử dụng rộng rãi trong các bản phát hành, nhưng các tiện ích kiểm thử không tuân theo các quy tắc ổn định về khả năng tương thích nhị phân giống như các API phát hành công khai.
Trong nhóm tài nguyên kiểm thử, hãy chú thích các lớp hoặc phương thức kiểm thử bằng @androidx.annotation.OptIn, thêm lựa chọn sử dụng package-info cho các gói kiểm thử hoặc định cấu hình lint.xml cho các tài nguyên kiểm thử. Để biết thêm thông tin, hãy xem
ghi chú khắc phục sự cố về tìm lỗi mã nguồn API không ổn định.
Kiểm thử cục bộ bằng Robolectric
Các chương trình chạy thử nghiệm Robolectric cục bộ chạy trên JVM của máy phát triển mà không cần thiết bị thực hoặc trình mô phỏng. Robolectric không cung cấp các phương thức triển khai MediaCodec và giữ cho Looper chính ở trạng thái tạm dừng cho đến khi được bơm rõ ràng, vì vậy, một thực thể ExoPlayer mặc định không thể tự giải mã nội dung nghe nhìn hoặc cung cấp thông tin cập nhật về trạng thái.
Tạo trình phát thử nghiệm và phát nâng cao
Sử dụng TestExoPlayerBuilder để tạo một ExoPlayer được định cấu hình cho các kiểm thử Robolectric. Theo mặc định, TestExoPlayerBuilder sẽ định cấu hình
các thành phần sau:
FakeClock(isAutoAdvancing = true) tự động chuyển tiếp, điều phối tiến trình thời gian giữa luồng phát trong nền củaExoPlayervàLooperchính bị tạm dừng của Robolectric.- Trình kết xuất video và âm thanh giả (
FakeVideoRenderervàFakeAudioRenderer) sử dụng các mẫu nội dung nghe nhìn theo dấu thời gian mà không cần bộ giải mãMediaCodec.
Để tiến hành phát và xác nhận kết quả trên luồng chính (với trình phát sử dụng Looper.getMainLooper, mặc định trong Robolectric), hãy sử dụng các phương thức advance(player) hoặc play(player) tĩnh trên TestPlayerRunHelper (play(player) gọi player.play trước khi chờ). Đối với hầu hết các điều kiện, trình trợ giúp sẽ bơm Looper chính của Robolectric cho đến khi đáp ứng điều kiện mục tiêu và gửi một ngoại lệ nếu xảy ra lỗi phát nghiêm trọng hoặc không nghiêm trọng:
Kotlin
@RunWith(AndroidJUnit4::class) @OptIn(UnstableApi::class) class PlaybackRobolectricTest { private lateinit var player: ExoPlayer @After fun tearDown() { if (::player.isInitialized) { player.release() } } @Test fun play_sampleMediaItem_reachesEndedState() { val context = ApplicationProvider.getApplicationContext<Context>() player = TestExoPlayerBuilder(context).build() player.setMediaItem(MediaItem.fromUri("asset:///media/mp4/sample.mp4")) player.prepare() play(player).untilState(Player.STATE_ENDED) assertThat(player.playbackState).isEqualTo(Player.STATE_ENDED) } }
Java
@RunWith(AndroidJUnit4.class) @OptIn(markerClass = UnstableApi.class) public static class PlaybackRobolectricTest { private @Nullable ExoPlayer player; @After public void tearDown() { if (player != null) { player.release(); } } @Test public void play_sampleMediaItem_reachesEndedState() throws Exception { Context context = ApplicationProvider.getApplicationContext(); player = new TestExoPlayerBuilder(context).build(); player.setMediaItem(MediaItem.fromUri("asset:///media/mp4/sample.mp4")); player.prepare(); play(player).untilState(Player.STATE_ENDED); assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_ENDED); } }
Trong ví dụ này, asset:///media/mp4/sample.mp4 đề cập đến một tệp nội dung nghe nhìn mẫu
được đặt trong thư mục src/test/assets/ (hoặc src/main/assets/) của mô-đun.
Khi được truyền một tệp đối chiếu ExoPlayer, advance(player) và play(player) sẽ trả về một ExoPlayerRunResult (hoặc PlayerRunResult khi được truyền một Player). Các loại kết quả này cung cấp các phương thức để chờ trạng thái phát lại, vị trí, chuyển đổi danh sách phát, khung hình được kết xuất, lệnh đang chờ xử lý và lỗi phát lại dự kiến hoặc bỏ qua lỗi không nghiêm trọng. Để biết thêm thông tin về điều kiện chờ, hãy xem tài liệu tham khảo API TestPlayerRunHelper.
Kiểm thử bằng nguồn giả và bộ mã hoá và giải mã mô phỏng
Nhiều kiểm thử tích hợp xác minh việc điều hướng danh sách phát, lựa chọn bản nhạc, cập nhật dòng thời gian hoặc hành vi thử lại và không cần tệp nội dung nghe nhìn thực. Bạn có thể cung cấp các dòng thời gian và dữ liệu tổng hợp vào trình phát TestExoPlayerBuilder bằng cách sử dụng FakeMediaSource, FakeTimeline, FakeDataSource hoặc WebServerDispatcher (với OkHttp MockWebServer):
Kotlin
val player = TestExoPlayerBuilder(context).build() val fakeMediaSource = FakeMediaSource(FakeTimeline(/* windowCount= */ 2)) player.setMediaSource(fakeMediaSource) player.prepare() play(player).untilMediaItemIndex(/* mediaItemIndex= */ 1) assertThat(player.currentMediaItemIndex).isEqualTo(1) player.release()
Java
ExoPlayer player = new TestExoPlayerBuilder(context).build(); FakeMediaSource fakeMediaSource = new FakeMediaSource(new FakeTimeline(/* windowCount= */ 2)); player.setMediaSource(fakeMediaSource); player.prepare(); play(player).untilMediaItemIndex(/* mediaItemIndex= */ 1); assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); player.release();
TestExoPlayerBuilder sử dụng trình kết xuất giả theo mặc định. Nếu chương trình kiểm thử Robolectric của bạn cần tương tác với MediaCodec, hãy định cấu hình bộ mã hoá và giải mã mô phỏng cũng như thu thập kết quả như sau:
- Thêm
withAllDefaultSupportedCodecstừShadowMediaCodecConfigdưới dạng@RuleJUnit để đăng ký bộ mã hoá và giải mã mô phỏng của Robolectric. - Truyền
CapturingRenderersFactoryđược tạo bằngClock(CapturingRenderersFactory(context, builder.getClock())) của trình tạo đếnbuilder.setRenderersFactory. - Khi kiểm thử các bản video, hãy đính kèm
Surface(chẳng hạn nhưSurface(SurfaceTexture(1))) vớiplayer.setVideoSurfaceđểMediaCodecVideoRendererkết xuất các khung hình đã giải mã thay vì bỏ qua. - Để xác minh kết quả hiển thị, hãy gọi
register(player, capturingRenderersFactory)trênPlaybackOutputtrước khi bắt đầu phát. - Sau khi phát xong, hãy giải phóng trình phát và nền tảng, sau đó kiểm tra kết quả được định dạng từ
Dumper(Dumper().add(playbackOutput).toString()).
Kiểm thử đo lường trên thiết bị
Các hoạt động kiểm thử Robolectric cục bộ không thực thi bộ giải mã nền tảng thực, bản âm thanh, phiên DRM hoặc kết xuất Surface. Để xác minh hành vi phát từ đầu đến cuối trên các thiết bị chạy Android, hãy viết kiểm thử đo lường trong src/androidTest bằng cách sử dụng ExoPlayer thực được tạo bằng ExoPlayer.Builder.
Trong kiểm thử đo lường JUnit, các phương thức @Test chạy trên một luồng đo lường thay vì luồng chính của ứng dụng. Luôn tạo, kiểm soát và phát hành ExoPlayer trên luồng ứng dụng Looper, ví dụ: bằng cách sử dụng runBlocking(Dispatchers.Main) trong Kotlin hoặc InstrumentationRegistry.getInstrumentation().runOnMainSync trong Java.
Chờ trạng thái của người chơi bằng PlayerFence và coroutine
Trong kiểm thử đo lường, Looper chính chạy theo thời gian thực trên thiết bị.
Đừng đưa Robolectric hoặc media3-test-utils-robolectric vào các phần phụ thuộc androidTestImplementation. Để chờ trạng thái và sự kiện phát, media3-test-utils cung cấp các trình trợ giúp sau:
- Kotlin: Tạm ngưng các hàm tiện ích trên
Player(chẳng hạn nhưawaitPlaybackState,awaitIsPlaying,awaitFirstFrameRenderedvàawaitContentPositionAtLeast). - Java: Phương thức
futureWhen(player)tĩnh trênPlayerFence, trả về mộtPlayerFencecó các phương thức điều kiện (chẳng hạn nhưentersPlaybackStatevàisPlaying) phải được gọi trên luồngLoopercủa trình phát và trả về mộtListenableFuturemà luồng kiểm thử có thể chặn bằngget.
Cả hai API đều giám sát người chơi trong khi chờ đợi. Nếu xảy ra lỗi phát nghiêm trọng (hoặc theo mặc định là lỗi không nghiêm trọng) hoặc nếu thời gian chờ vượt quá thời gian chờ (mặc định là 10 giây hoặc 30 giây trên trình mô phỏng), thì lệnh gọi sẽ không thành công ngay lập tức thay vì bị treo:
Kotlin
@RunWith(AndroidJUnit4::class) @OptIn(UnstableApi::class) class PlaybackInstrumentationTest { private lateinit var player: ExoPlayer @After fun tearDown() = runBlocking(Dispatchers.Main) { if (::player.isInitialized) { player.release() } } @Test fun play_sampleMediaItem_advancesAndEnds() = runBlocking(Dispatchers.Main) { val context = ApplicationProvider.getApplicationContext<Context>() player = ExoPlayer.Builder(context).build() player.setMediaItem(MediaItem.fromUri("asset:///media/mp4/sample.mp4")) player.prepare() player.play() player.awaitIsPlaying(true) player.awaitContentPositionAtLeast(500) player.awaitPlaybackState(Player.STATE_ENDED) assertThat(player.playbackState).isEqualTo(Player.STATE_ENDED) } }
Java
@RunWith(AndroidJUnit4.class) @OptIn(markerClass = UnstableApi.class) public static class PlaybackInstrumentationTest { private @Nullable ExoPlayer player; @After public void tearDown() { InstrumentationRegistry.getInstrumentation() .runOnMainSync(() -> { if (player != null) { player.release(); } }); } @Test public void play_sampleMediaItem_advancesAndEnds() throws Exception { SettableFuture<Void> playbackStarted = SettableFuture.create(); SettableFuture<Void> positionPassed = SettableFuture.create(); SettableFuture<Void> playbackEnded = SettableFuture.create(); InstrumentationRegistry.getInstrumentation() .runOnMainSync(() -> { Context context = ApplicationProvider.getApplicationContext(); player = new ExoPlayer.Builder(context).build(); player.setMediaItem(MediaItem.fromUri("asset:///media/mp4/sample.mp4")); playbackStarted.setFuture(futureWhen(player).isPlaying(true)); positionPassed.setFuture(futureWhen(player).passesContentPosition(500)); playbackEnded.setFuture(futureWhen(player).entersPlaybackState(Player.STATE_ENDED)); player.prepare(); player.play(); }); playbackStarted.get(); positionPassed.get(); playbackEnded.get(); InstrumentationRegistry.getInstrumentation() .runOnMainSync(() -> assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_ENDED)); } }
Khi tạo ExoPlayer bằng ApplicationProvider.getApplicationContext trong một mô-đun ứng dụng (com.android.application), URI asset:/// sẽ phân giải dựa trên các thành phần của ứng dụng mục tiêu (chẳng hạn như src/debug/assets/ hoặc src/main/assets/, thay vì src/androidTest/assets/).
Sự kiện tạm thời và hoạt động giám sát tuỳ chỉnh
Ngoài việc kiểm tra trạng thái hiện tại của trình phát (trong đó các lệnh chờ sẽ trả về ngay lập tức nếu điều kiện đã được đáp ứng), các trình trợ giúp trong media3-test-utils còn hỗ trợ các sự kiện một lần và điều kiện trình nghe tuỳ chỉnh:
- Sự kiện tạm thời, chẳng hạn như khung hình được kết xuất đầu tiên: Đính kèm đầu ra video (chẳng hạn như
SurfacehoặcPlayerView) vào trình phát và bắt đầu chờ bằngawaitFirstFrameRenderedhoặcrendersFirstFrametrước khi gọiplayer.prepaređể không bỏ lỡ sự kiện khung hình một lần sớm. - Điều kiện tuỳ chỉnh: Gói trình nghe tuỳ chỉnh hoặc coroutine chờ bên trong
awaitSupervisedtrênplayertrong Kotlin hoặc truyềnListenableFuturetuỳ chỉnh đếnfutureWhen(player).withSupervision(future)trong Java. Khi kiểm thử khả năng khôi phục sau lỗi không nghiêm trọng, hãy truyềnfailOnNonFatalErrors = false(Kotlin) hoặc gọiignoringNonFatalErrors(Java) và tuỳ chỉnh thời gian chờ bằngtimeouthoặcwithTimeoutMs(timeoutMs).
Ví dụ sau đây chờ một sự kiện khung hình đầu tiên tạm thời và giám sát một trình nghe siêu dữ liệu tuỳ chỉnh:
Kotlin
// Start awaiting transient events (with a video surface attached) before prepare(). coroutineScope { val firstFrameDeferred = async(start = CoroutineStart.UNDISPATCHED) { player.awaitFirstFrameRendered() } player.setMediaItem(mediaItem) player.prepare() player.play() firstFrameDeferred.await() } // Supervise a custom listener condition while still failing fast on player errors. val metadataTitle = player.awaitSupervised { player.mediaMetadata.title?.let { return@awaitSupervised it } val titleDeferred = CompletableDeferred<CharSequence>() val listener = object : Player.Listener { override fun onMediaMetadataChanged(mediaMetadata: MediaMetadata) { mediaMetadata.title?.let { titleDeferred.complete(it) } } } player.addListener(listener) try { titleDeferred.await() } finally { player.removeListener(listener) } }
Java
SettableFuture<Void> firstFrameFuture = SettableFuture.create(); SettableFuture<CharSequence> metadataTitleFuture = SettableFuture.create(); SettableFuture<CharSequence> supervisedTitleFuture = SettableFuture.create(); Player.Listener metadataListener = new Player.Listener() { @Override public void onMediaMetadataChanged(MediaMetadata mediaMetadata) { if (mediaMetadata.title != null) { metadataTitleFuture.set(mediaMetadata.title); } } }; try { InstrumentationRegistry.getInstrumentation() .runOnMainSync(() -> { // Register transient event fences (with a video surface attached) before prepare(). firstFrameFuture.setFuture(futureWhen(player).rendersFirstFrame()); // Supervise a custom ListenableFuture so player errors fail fast. player.addListener(metadataListener); supervisedTitleFuture.setFuture( futureWhen(player).withSupervision(metadataTitleFuture)); player.setMediaItem(mediaItem); if (player.getMediaMetadata().title != null) { metadataTitleFuture.set(player.getMediaMetadata().title); } player.prepare(); player.play(); }); firstFrameFuture.get(); CharSequence title = supervisedTitleFuture.get(); } finally { InstrumentationRegistry.getInstrumentation() .runOnMainSync(() -> { player.removeListener(metadataListener); firstFrameFuture.cancel(/* mayInterruptIfRunning= */ true); supervisedTitleFuture.cancel(/* mayInterruptIfRunning= */ true); }); }
Kiểm thử giao diện người dùng và các thành phần của ứng dụng bằng FakePlayer
Khi kiểm thử các thành phần giao diện người dùng như thành phần kết hợp PlayerView hoặc media3-ui-compose, ViewModel hoặc lệnh gọi lại MediaSession, bạn thường không cần tạo ExoPlayer đầy đủ. FakePlayer trong media3-test-utils mở rộng SimpleBasePlayer để mô phỏng danh sách phát, trạng thái phát, vị trí, kích thước video, tín hiệu và lỗi trên Looper của luồng gọi. Looper này tự động có sẵn trong các kiểm thử Robolectric có @RunWith(AndroidJUnit4::class) và trên luồng chính trong kiểm thử đo lường.
Theo mặc định, FakePlayer mô phỏng độ trễ lưu vào vùng đệm 100 mili giây trên prepare và seekTo; truyền bufferingDelayMs = 0 trong Kotlin hoặc đặt trạng thái trực tiếp bằng setPlaybackState:
Kotlin
val fakePlayer = FakePlayer(bufferingDelayMs = 0) fakePlayer.setMediaItem(MediaItem.fromUri("https://example.com/media.mp4")) fakePlayer.prepare() fakePlayer.play() assertThat(fakePlayer.isPlaying).isTrue() // Drive state transitions directly to test UI or session listeners. fakePlayer.setPosition(30_000) fakePlayer.setPlaybackState(Player.STATE_ENDED) fakePlayer.release()
Java
FakePlayer fakePlayer = new FakePlayer(); fakePlayer.setMediaItem(MediaItem.fromUri("https://example.com/media.mp4")); // Drive state transitions directly (prepare() simulates a 100 ms buffering delay by default). fakePlayer.setPlaybackState(Player.STATE_READY); fakePlayer.play(); assertThat(fakePlayer.isPlaying()).isTrue(); fakePlayer.setPosition(30_000); fakePlayer.setPlaybackState(Player.STATE_ENDED); fakePlayer.release();
Các tiện ích kiểm thử khác
media3-test-utils bao gồm các trình trợ giúp chuyên biệt cho các trường hợp kiểm thử khác:
- Cờ tính năng và chế độ Canary: Sử dụng
Media3FlagsRulecùng với@BindFlagvà@BindCanaryModeđể đặt lại các giá trị ghi đè cờ sau mỗi lần kiểm thử và chạy các kiểm thử có tham số trên các trạng thái cờ. Để biết thêm thông tin, hãy xem phần kiểm thử bằng cờ tính năng. - Kiểm thử hợp đồng
DataSourcetuỳ chỉnh: Lớp conDataSourceContractTestđể xác minh rằng các phương thức triển khaiDataSourcehoặcHttpDataSourcetuỳ chỉnh tuân thủ các kỳ vọng về hợp đồng của Media3 trên các yêu cầu về phạm vi, xử lý lỗi và trình nghe chuyển. Để biết thêm thông tin, hãy xem phần tích hợp các bộ phần cứng và phần mềm mạng khác.