Developer Console

Optimize the performance of your Fire TV app

Giovanni Laquidara Dec 05, 2024
Share:
Fire TV App performance Best practices
Blog_Header_Post_Img

When you sit down to watch some TV after work, the last thing you want to stare at a spinning circle as you wait for the app to load. The same goes for your customers. The last thing your customers want to think about is how well your app is performing. They just want it to work seamlessly, without any spinning circles or delays. To provide a smooth, responsive streaming experience, let's explore three common Fire TV app performance issues and how to fix them.

 

Slow startup time

Your app startup time is your user's first interaction with your app, and it sets the tone for their entire experience. A slow or unresponsive startup can lead to frustration and even app abandonment before the user has a chance to engage with your content. Understanding and optimizing your app's startup performance is crucial for making a strong first impression and retaining users.


Understanding TTID and TTFD

To effectively measure and improve startup performance, it's important to understand two key metrics:

  1. Time to Initial Display (TTID): This is the time it takes for your app to display its first frame after launch. It represents how quickly users see something on the screen, indicating that the app is responsive.
  2. Time to Full Display (TTFD): This measures the time it takes for your app to become fully interactive, with all critical UI elements and initial content loaded.


Retrieving TTID

To find your app's TTID, look for the "Displayed" value in Logcat:

Retrieving TTID

Measuring TTFD

To measure TTFD, use the reportFullyDrawn() method:

Copied to clipboard
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            HelloWorldTVTheme {
                // Your app content
            }
        }
        // Call this when your app is fully loaded and interactive
        reportFullyDrawn()
    }
}
Measuring TTFD

Measuring TTID and TTFD with Jetpack Compose

If you are using Jetpack Compose for your TV app, you can use specific APIs to help you accurately measure the previous metrics.

  • ReportDrawn(): This API indicates that your composable is immediately ready for interaction. It's typically used to measure TTID.

Copied to clipboard
@Composable
fun HomeScreen() {
    ReportDrawn()
    // Your UI content
}

  • ReportDrawnWhen(): This API takes a predicate and reports the drawn state when the condition is met. It's useful when your UI readiness depends on certain data being loaded.

 

Copied to clipboard
@Composable
fun ContentList(items: List<Item>) {
    ReportDrawnWhen(items.size > 0)
    LazyColumn {
        items(items) { item ->
            ItemRow(item)
        }
    }
}

  • ReportDrawnAfter(): This API takes a suspending function and reports the drawn state after the function completes. It's particularly useful for operations that may take some time, like loading initial data.

 

Copied to clipboard
@Composable
fun DataDependentScreen(viewModel: MyViewModel) {
    ReportDrawnAfter {
        viewModel.loadInitialData()
    }
    // UI that depends on the loaded data
}

To explore more of these and other measurements you can also leverage the open-source FireOS Performance Testing tool, to analyze your app on a real device connected to your computer, producing comprehensive logs for your optimization process.

 

How to speed up your TTID and TTFD

If you discover that your TTID and TTFD numbers are too big (for example more of the values stated here), your app’s startup performance may be impacted by one of these common issues:

  1. Layout complexity: Inflating large or deeply nested layouts
  2. I/O operations: Executing disk or network operations on the main thread before the starting of the Main Activity or in the Application class
  3. Image processing: Loading and decoding large images inefficiently at the start

Inefficient video playback management

An animated GIF showing a spinning loading indicator, indicating that the video player is initializing

Video playback is at the heart of any streaming app. Performance issues and poor user experience are often caused by improper management of the player instance.

This code example contains some common video playback management mistakes. Take a look to see if you can find them.

Copied to clipboard
class VideoPlayerActivity : ComponentActivity() {
    private lateinit var player: ExoPlayer

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        player = ExoPlayer.Builder(this).build()
        val mediaItem = MediaItem.fromUri("https://example.com/video.mp4")
        player.setMediaItem(mediaItem)
        player.prepare()

        setContent {
            VideoPlayerComposable(player)
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        player.release()
    }
}

@Composable
fun VideoPlayerComposable(player: ExoPlayer) {
    AndroidView({ context ->
        PlayerView(context).apply {
            this.player = player
        }
    })
}

Did you found them? Here are the issues:

  1. The player is created and prepared in onCreate, potentially delaying other UI initialization.
  2. There's no handling of configuration changes, which can lead to unnecessary player reinitialization.
  3. There's no visual feedback for the user while the player is preparing.
  4. Error handling is absent, which can lead to crashes or unresponsive UI.

 

How to fix the issues

Copied to clipboard
class VideoPlayerViewModel : ViewModel() {
    private val _playerState = MutableStateFlow<PlayerState>(PlayerState.Initializing)
    val playerState: StateFlow<PlayerState> = _playerState

    private var player: ExoPlayer? = null

    fun initializePlayer(context: Context) {
        // Ensure this is called on the main thread
        player = ExoPlayer.Builder(context).build().apply {
            val mediaItem = MediaItem.fromUri("https://example.com/video.mp4")
            setMediaItem(mediaItem)
            // Prepare asynchronously
            prepare()
            addListener(object : Player.Listener {
                override fun onPlaybackStateChanged(state: Int) {
                    when (state) {
                        Player.STATE_READY -> _playerState.value = PlayerState.Ready(this@apply)
                        Player.STATE_BUFFERING -> _playerState.value = PlayerState.Buffering
                        Player.STATE_ENDED -> _playerState.value = PlayerState.Ended
                        Player.STATE_IDLE -> _playerState.value = PlayerState.Idle
                    }
                }

                override fun onPlayerError(error: PlaybackException) {
                    _playerState.value = PlayerState.Error(error.message ?: "Unknown error")
                }
            })
        }
    }

    fun releasePlayer() {
        player?.release()
        player = null
        _playerState.value = PlayerState.Initializing
    }

    override fun onCleared() {
        releasePlayer()
        super.onCleared()
    }
}

sealed class PlayerState {
    object Initializing : PlayerState()
    object Buffering : PlayerState()
    class Ready(val player: ExoPlayer) : PlayerState()
    object Ended : PlayerState()
    object Idle : PlayerState()
    class Error(val message: String) : PlayerState()
}

@Composable
fun VideoPlayerScreen(viewModel: VideoPlayerViewModel = viewModel()) {
    val playerState by viewModel.playerState.collectAsState()
    val context = LocalContext.current

    LaunchedEffect(Unit) {
        viewModel.initializePlayer(context)
    }

    DisposableEffect(Unit) {
        onDispose {
            viewModel.releasePlayer()
        }
    }

    when (val state = playerState) {
        is PlayerState.Initializing, PlayerState.Buffering -> LoadingIndicator()
        is PlayerState.Ready -> VideoPlayer(state.player)
        is PlayerState.Error -> ErrorMessage(state.message)
        PlayerState.Ended -> PlaybackEndedUI()
        PlayerState.Idle -> IdleStateUI()
    }
}

@Composable
fun VideoPlayer(player: ExoPlayer) {
    AndroidView(
        factory = { context ->
            PlayerView(context).apply {
                this.player = player
            }
        },
        modifier = Modifier.fillMaxSize()
    )
}

We used a ViewModel and Compose's LaunchedEffect and DisposableEffect to handle proper initialization and cleanup of the video player;

 

Copied to clipboard
    LaunchedEffect(Unit) {
        viewModel.initializePlayer(context)
    }
    DisposableEffect(Unit) {
        onDispose {
            viewModel.releasePlayer()
        }
    }

Plus, we added a loading indicator and error messages to provide a better feedback to the user and covered playback errors preventing crashes.

 

Copied to clipboard
 when (val state = playerState) {
        is PlayerState.Initializing, PlayerState.Buffering -> LoadingIndicator()
        is PlayerState.Ready -> VideoPlayer(state.player)
        is PlayerState.Error -> ErrorMessage(state.message)
        PlayerState.Ended -> PlaybackEndedUI()
        PlayerState.Idle -> IdleStateUI()
    }

Unoptimized image loading

A screenshot of a mobile app UI displaying a gallery of movie poster thumbnails. The thumbnails are arranged in a grid layout, showcasing various movie titles and cover art

Your Fire TV app likely showcases many images in the same screens such as movie posters, show thumbnails, actor headshots, and more. The way these images are loaded can significantly impact your app's performance. You may be tempted to use something like this to draw one of these images composable on screen:

 

Copied to clipboard
@Composable
fun UnoptimizedThumbnail(url: String) {
    AsyncImage(
        model = url,
        contentDescription = null,
        modifier = Modifier.size(200.dp)
    )
}

This kind of component can lead to three main issues:

  1. Excessive Memory Usage: Loading full-size images for thumbnail displays wastes memory.
  2. Slow loading UI: Large images take longer to download and process, leading to a slow interface.
  3. Stuttering Scrolling: When scrolling through the content lists, poor image loading can cause janky scrolling.

This is because the code is hiding several performance pitfalls:

  • It loads the full-size image from the URL, regardless of the display size.
  • There's no resizing happening on the network level, meaning we may be downloading more data than necessary.
  • The device uses more CPU power to eventually resize the image for display, potentially causing UI hiccups.

 

Fix:

To address these issues you can use an image loading library ( like Coil )

Copied to clipboard
@Composable
fun OptimizedThumbnail(url: String) {
    AsyncImage(
        model = ImageRequest.Builder(LocalContext.current)
            .data(url)
            .size(width = 200, height = 200)
            .crossfade(true)
            .build(),
        contentDescription = null,
        contentScale = ContentScale.Crop,
        modifier = Modifier.size(200.dp)
    )
}

Let's break down the improvements:

  1. Size specification:
    • .size(width = 200, height = 200) tells Coil to resize the image to 200x200 pixels, ideally on the server or during download. This significantly reduces the amount of data transferred and the memory used to store the image.
  2. Crossfade effect: .
    • .crossfade(true) adds a smooth transition effect when loading images. This improves the perceived performance, making image loading feel more seamless.
  3. Content scaling:
    • contentScale = ContentScale.Crop ensures the image fills the entire space without distortion. This provides a consistent look across different image aspect ratios.

Wrapping up

These optimizations form the foundation of a high-performance Fire TV apps. By implementing these techniques, you're well on your way to creating a streaming experience that stands out in the competitive world of TV apps and makes your user happy.
However, the journey to optimal performance doesn't end here. In future articles, we'll explore additional optimization!

Stay tuned and keep optimizing!

 

Related articles

Sign up for our newsletter

Stay up to date with the latest Amazon Developer news, industry trends and blog posts.