Mobile Development 20 min read

Video Player Architecture Refactoring and Performance Optimization in an Android Short‑Video App

The article details how refactoring the Haokan Android short‑video client from a single global player to per‑holder multi‑player instances, combined with lifecycle‑lite event handling, pre‑loading, and dedicated thread operations, cut start‑up latency by ~150 ms, reduced frame‑drops dramatically, and boosted overall user experience and business metrics.

Baidu Geek Talk
Baidu Geek Talk
Baidu Geek Talk
Video Player Architecture Refactoring and Performance Optimization in an Android Short‑Video App

Background: In short‑video applications the player is the most important component; its performance directly affects core user experience. The article shares the ideas and experiences of refactoring the Haokan Video Android client, focusing on architecture and performance optimization.

Old architecture – single global player: The player was a singleton that floated on top of all Views, attached to ViewPager and RecyclerView . This gave the player a very large lifecycle and scope, tightly coupled business logic, complex state control, and caused noticeable start‑up pauses, especially on low‑end devices.

Key problems of the old design:

Severe business coupling – >10k lines of core code, many hidden/show logic branches, making maintenance hard.

Complex and chaotic player state control across Activity, Fragment, ViewPager, RecyclerView, etc., leading to difficult bug tracing.

Performance bottlenecks – the player’s top‑layer position caused view‑overlap issues, ANR, and frame drops during feed scrolling.

Example of the old listener registration:

mViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
    @Override
    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
        mVideoView.moveVideoViewByX(positionOffsetPixels);
    }
});

mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
    @Override
    public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
        mVideoView.moveVideoViewByY(dy);
    }
});

New architecture – multi‑player instances: Each holder now owns its own player, reducing the player’s scope and lifecycle. This decouples business logic from the player, simplifies state management, and improves scrolling smoothness.

Benefits of the new design:

Each holder manages its own player, allowing independent control of ads, live streams, etc.

LifecycleLite replaces EventBus for event distribution, reducing memory‑leak risk.

Custom PageSnapHelper and other components optimize feed start‑play and pre‑loading.

Performance impact: Frame‑drop counts dropped from 350 to 150 (light) and from 77 to 18 (severe) per 10 minutes after refactor. Start‑up latency improved by ~150 ms, achieving near‑instant playback.

Start‑time optimization:

1. Player creation timing – initialize the next video’s player in onBindViewHolder of the RecyclerView holder.

@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
    if (holder instanceof ImmersiveBaseHolder) {
        ((ImmersiveBaseHolder) holder).onBind(getData(position), position);
        holder.createPlayer();
    }
}

2. Start playback timing – trigger prepareAsync when scrolling stops (in onScrollStateChanged ) and start the player only after the scroll settles.

mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
    @Override
    public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
        if (newState == SCROLL_STATE_SETTLING) {
            currentHolder.player.prepareAsync();
            lastHolder.player.stopAndRelease();
        }
    }
});

Further optimization explores starting playback as soon as the target holder is identified by PagerSnapHelper (almost “instant play”).

@Override
protected LinearSmoothScroller createSnapScroller(RecyclerView.LayoutManager layoutManager) {
    return new LinearSmoothScroller(mRecyclerView.getContext()) {
        @Override
        protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
            int nextPosition = state.getTargetScrollPosition();
            adapter.getHolder(nextPosition).player.start();
            adapter.getHolder(currentPosition).player.stopAndRelease();
        }
    };
}

Pre‑loading strategy: Load the first 300‑500 KB of a video (enough for several frames) before playback. The article modifies the third‑party AndroidVideoCache library to support fixed‑size pre‑load and stop‑preload operations.

static final int PRELOAD_CACHE_SIZE = 300 * 1024;

public void preload(Context context, String url, int preloadSize) {
    socketProcessor.submit(new PreloadProcessorRunnable(url, preloadSize));
}

private final class PreloadProcessorRunnable implements Runnable {
    private final String url;
    private int preloadSize = PRELOAD_CACHE_SIZE;
    public PreloadProcessorRunnable(String url, int preloadSize) {
        this.url = url;
        this.preloadSize = preloadSize;
    }
    @Override
    public void run() {
        processPreload(url, preloadSize);
    }
}

private void processPreload(String url, int preloadSize) throws IOException, ProxyCacheException {
    long cacheAvailable = cache.available();
    if (cacheAvailable < preloadSize) {
        byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
        int readBytes;
        long offset = cacheAvailable;
        while ((readBytes = read(buffer, offset, buffer.length)) != -1) {
            offset += readBytes;
            if (offset > preloadSize) break;
        }
        ProxyLogUtil.d(TAG, "preloaded url = " + source.getUrl() + ", offset = " + offset + ", preloadSize = " + preloadSize);
    }
}

public void stopPreload(String url) {
    try {
        HttpProxyCacheServerClients clients = getClientsWithoutNew(url);
        if (clients != null) {
            clients.shutdown();
        }
    } catch (ProxyCacheException e) {
        e.printStackTrace();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

Player stutter and crash handling: Creation and release of the player are heavy operations that can block the UI thread and cause ANR. The article suggests moving all player operations to a dedicated HandlerThread and adding an isPlayerReleased flag to guard against calls after release.

static long ijkmp_get_duration(IjkMediaPlayer *mp) {
    assert(mp);
    pthread_mutex_lock(&mp->mutex);
    long retval = ijkmp_get_duration_l(mp);
    pthread_mutex_unlock(&mp->mutex);
    return retval;
}

static long ijkmp_get_duration_l(IjkMediaPlayer *mp) {
    if (mp == NULL) {
        return 0;
    }
    return ffp_get_duration_l(mp->ffplayer);
}
// NOTICE: This still has thread‑conflict issues

Overall impact: Development efficiency improved by at least 20 %, start‑up latency reduced by ~150 ms (near‑instant playback), frame‑drop rates dramatically lowered, and business metrics such as retention, video views per user, and monetization all showed noticeable gains.

The article concludes that technical optimizations, when tied to clear business outcomes, become compelling reasons for investment and can be validated through A/B experiments.

architecturePerformanceOptimizationAndroidRecyclerViewPreloadingVideoPlayer
Baidu Geek Talk
Written by

Baidu Geek Talk

Follow us to discover more Baidu tech insights.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.