> ## Documentation Index
> Fetch the complete documentation index at: https://www.cometchat.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Transcription & Closed Captions

> Use CometChat Calls SDK v5 transcription on Android to transcribe calls, show live closed captions, and retrieve transcripts after the call.

<Note>
  **Available since v5.0.4** — transcription and closed captions require CometChat Calls SDK v5.0.4 or later for Android. See [Setup](/docs/calls/android/setup) to install or upgrade.
</Note>

Transcribe call sessions in real time and display live closed captions on screen. Transcripts are stored server-side and can be retrieved after the call using `TranscriptRequest`.

<Warning>
  Transcription must be enabled for your CometChat app. Contact support if you need to enable this feature.
</Warning>

## How It Works

Transcription and closed captions are two related but separate things:

| Concept             | What it does                                                                                                                                                                   |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Transcription**   | Server-side speech-to-text for the session. Starting it brings a transcriber into the call, which produces the transcript that is stored for later retrieval.                  |
| **Closed captions** | The on-screen overlay that renders the live transcript as it arrives. Captions are produced from the running transcription, so they only appear while transcription is active. |

Starting transcription is a prerequisite for captions — toggling captions on without an active transcription shows nothing.

## Starting Transcription

### Auto-Start Transcription

Configure transcription to start automatically when the session begins:

<Tabs>
  <Tab title="Kotlin">
    ```kotlin theme={null}
    val sessionSettings = CometChatCalls.SessionSettingsBuilder()
        .enableAutoStartTranscription(true)
        // ... other settings
        .build()
    ```
  </Tab>

  <Tab title="Java">
    ```java theme={null}
    SessionSettings sessionSettings = new CometChatCalls.SessionSettingsBuilder()
        .enableAutoStartTranscription(true)
        // ... other settings
        .build();
    ```
  </Tab>
</Tabs>

**Default:** `false`

### Manual Transcription Control

Transcription can be started and stopped during an active call through the `CallSession` singleton.

#### Start Transcription

<Tabs>
  <Tab title="Kotlin">
    ```kotlin theme={null}
    val callSession = CallSession.getInstance()
    callSession.startTranscription()
    ```
  </Tab>

  <Tab title="Java">
    ```java theme={null}
    CallSession callSession = CallSession.getInstance();
    callSession.startTranscription();
    ```
  </Tab>
</Tabs>

#### Stop Transcription

Stops the current transcription. Any captions currently on screen are cleared:

<Tabs>
  <Tab title="Kotlin">
    ```kotlin theme={null}
    val callSession = CallSession.getInstance()
    callSession.stopTranscription()
    ```
  </Tab>

  <Tab title="Java">
    ```java theme={null}
    CallSession callSession = CallSession.getInstance();
    callSession.stopTranscription();
    ```
  </Tab>
</Tabs>

<Note>
  Always check `isSessionActive()` before calling these actions to ensure there's an active call.
</Note>

## Built-in UI Controls

On Android, both transcription controls live in the control panel's **More** menu and are hidden by default.

### Transcription Button

To show the **Start Transcription** / **Stop Transcription** item:

<Tabs>
  <Tab title="Kotlin">
    ```kotlin theme={null}
    val sessionSettings = CometChatCalls.SessionSettingsBuilder()
        .hideTranscriptionButton(false)
        .build()
    ```
  </Tab>

  <Tab title="Java">
    ```java theme={null}
    SessionSettings sessionSettings = new CometChatCalls.SessionSettingsBuilder()
        .hideTranscriptionButton(false)
        .build();
    ```
  </Tab>
</Tabs>

**Default:** `true`

The menu item toggles between **Start Transcription** and **Stop Transcription** based on the current state.

### Closed Caption Button

To show the **Show Captions** / **Hide Captions** item, which toggles the on-screen captions overlay:

<Tabs>
  <Tab title="Kotlin">
    ```kotlin theme={null}
    val sessionSettings = CometChatCalls.SessionSettingsBuilder()
        .hideClosedCaptionButton(false)
        .build()
    ```
  </Tab>

  <Tab title="Java">
    ```java theme={null}
    SessionSettings sessionSettings = new CometChatCalls.SessionSettingsBuilder()
        .hideClosedCaptionButton(false)
        .build();
    ```
  </Tab>
</Tabs>

**Default:** `true`

<Note>
  Even with `hideClosedCaptionButton(false)`, the captions item only appears once transcription is running for the session, because captions are generated from the live transcript.
</Note>

## Caption Language

**Method:** `setCaptionLanguage(String)`

Sets the language used for transcription and captions. This is fixed for the session — there is no in-call language picker on Android.

<Tabs>
  <Tab title="Kotlin">
    ```kotlin theme={null}
    val sessionSettings = CometChatCalls.SessionSettingsBuilder()
        .setCaptionLanguage("en-US")
        .build()
    ```
  </Tab>

  <Tab title="Java">
    ```java theme={null}
    SessionSettings sessionSettings = new CometChatCalls.SessionSettingsBuilder()
        .setCaptionLanguage("en-US")
        .build();
    ```
  </Tab>
</Tabs>

**Default:** `en-US`

<Accordion title="Supported Language Codes">
  | Code    | Language                               |
  | ------- | -------------------------------------- |
  | `en-US` | English (United States)                |
  | `de-DE` | German (Germany)                       |
  | `en-GB` | English (United Kingdom)               |
  | `es-ES` | Spanish (Spain)                        |
  | `fr-FR` | French (France)                        |
  | `hi-IN` | Hindi (India)                          |
  | `hu-HU` | Hungarian (Hungary)                    |
  | `it-IT` | Italian (Italy)                        |
  | `ja-JP` | Japanese (Japan)                       |
  | `ko-KR` | Korean (South Korea)                   |
  | `lt-LT` | Lithuanian (Lithuania)                 |
  | `ms-MY` | Malay (Malaysia)                       |
  | `nl-NL` | Dutch (Netherlands)                    |
  | `pt-PT` | Portuguese (Portugal)                  |
  | `ru-RU` | Russian (Russia)                       |
  | `sv-SE` | Swedish (Sweden)                       |
  | `tr-TR` | Turkish (Turkey)                       |
  | `zh`    | Chinese Mandarin (Simplified, China)   |
  | `zh-TW` | Chinese Mandarin (Traditional, Taiwan) |
</Accordion>

## Retrieving Transcripts

After a call, use `TranscriptRequest` to list the transcript artifacts for a session. Each record is a **pointer to a downloadable transcript file**, not the transcript text itself.

<Note>
  The SDK must be initialized with `CometChatCalls.init()` and a user must be logged in. The auth token is read from the logged-in user at fetch time, so it automatically tracks re-logins.
</Note>

### Building a Request

<Tabs>
  <Tab title="Kotlin">
    ```kotlin theme={null}
    val transcriptRequest = TranscriptRequest.TranscriptRequestBuilder()
        .setSessionId("v1.us.2547167fe69871fd.alice") // required
        .setLimit(10)                                  // optional
        .build()

    transcriptRequest.fetchNext(object : CometChatCalls.CallbackListener<List<Transcript>>() {
        override fun onSuccess(transcripts: List<Transcript>) {
            for (transcript in transcripts) {
                Log.d(TAG, "Transcript ID: ${transcript.tid}")
                Log.d(TAG, "Transcript URL: ${transcript.transcriptUrl}")
            }
        }

        override fun onError(e: CometChatException) {
            Log.e(TAG, "Error: ${e.code} ${e.message}")
        }
    })
    ```
  </Tab>

  <Tab title="Java">
    ```java theme={null}
    TranscriptRequest transcriptRequest = new TranscriptRequest.TranscriptRequestBuilder()
        .setSessionId("v1.us.2547167fe69871fd.alice") // required
        .setLimit(10)                                  // optional
        .build();

    transcriptRequest.fetchNext(new CometChatCalls.CallbackListener<List<Transcript>>() {
        @Override
        public void onSuccess(List<Transcript> transcripts) {
            for (Transcript transcript : transcripts) {
                Log.d(TAG, "Transcript ID: " + transcript.getTid());
                Log.d(TAG, "Transcript URL: " + transcript.getTranscriptUrl());
            }
        }

        @Override
        public void onError(CometChatException e) {
            Log.e(TAG, "Error: " + e.getCode() + " " + e.getMessage());
        }
    });
    ```
  </Tab>
</Tabs>

| Method                 | Required | Description                                                                                                                               |
| ---------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `setSessionId(String)` | Yes      | The session ID whose transcripts to fetch. A null or empty value is reported as `ERROR_INVALID_SESSIONID` on the first fetch.             |
| `setLimit(int)`        | No       | Page size. Defaults to `30` and is capped at `1000`. A value of `0` or less is reported as `ERROR_NON_POSITIVE_LIMIT` on the first fetch. |
| `build()`              | Yes      | Returns a `TranscriptRequest`. Never throws — validation happens at fetch time.                                                           |

### Paginating

A `TranscriptRequest` is a stateful cursor. Create one per session ID and drive it with `fetchNext()` and `fetchPrevious()`:

<Tabs>
  <Tab title="Kotlin">
    ```kotlin theme={null}
    // Fetch next page
    transcriptRequest.fetchNext(object : CometChatCalls.CallbackListener<List<Transcript>>() {
        override fun onSuccess(transcripts: List<Transcript>) {
            if (transcripts.isEmpty()) {
                // No more pages
            }
        }

        override fun onError(e: CometChatException) {
            Log.e(TAG, "Error: ${e.message}")
        }
    })

    // Fetch previous page
    transcriptRequest.fetchPrevious(object : CometChatCalls.CallbackListener<List<Transcript>>() {
        override fun onSuccess(transcripts: List<Transcript>) {
            // Handle previous page
        }

        override fun onError(e: CometChatException) {
            Log.e(TAG, "Error: ${e.message}")
        }
    })
    ```
  </Tab>

  <Tab title="Java">
    ```java theme={null}
    // Fetch next page
    transcriptRequest.fetchNext(new CometChatCalls.CallbackListener<List<Transcript>>() {
        @Override
        public void onSuccess(List<Transcript> transcripts) {
            if (transcripts.isEmpty()) {
                // No more pages
            }
        }

        @Override
        public void onError(CometChatException e) {
            Log.e(TAG, "Error: " + e.getMessage());
        }
    });

    // Fetch previous page
    transcriptRequest.fetchPrevious(new CometChatCalls.CallbackListener<List<Transcript>>() {
        @Override
        public void onSuccess(List<Transcript> transcripts) {
            // Handle previous page
        }

        @Override
        public void onError(CometChatException e) {
            Log.e(TAG, "Error: " + e.getMessage());
        }
    });
    ```
  </Tab>
</Tabs>

* `fetchNext()` delivers the next page, or an empty list when there are no more pages. A session with no transcripts delivers an empty list.
* `fetchPrevious()` delivers the previous page, or an empty list when already on the first page. It never requests a page below `1`.
* Only one fetch may be in flight at a time. Calling `fetchNext()` or `fetchPrevious()` while another request is pending reports `ERROR_REQUEST_IN_PROGRESS`; the original call is unaffected and still completes.
* Callbacks are always delivered on the main thread.

### Transcript Object

| Property         | Type       | Description                                                                                         |
| ---------------- | ---------- | --------------------------------------------------------------------------------------------------- |
| `tid`            | String     | Transcript ID                                                                                       |
| `mid`            | String     | Meeting ID                                                                                          |
| `roomName`       | String     | Room name of the meeting                                                                            |
| `startTime`      | long       | Meeting start time, in epoch **seconds**                                                            |
| `endTime`        | long       | Meeting end time, in epoch **seconds**                                                              |
| `url`            | String     | Meeting URL                                                                                         |
| `transcriptDate` | String     | Transcript date                                                                                     |
| `transcriptUrl`  | String     | URL of the downloadable transcript JSON                                                             |
| `metaData`       | JSONObject | The full raw record as returned by the server, so any fields not modelled above are still available |

<Note>
  Every property is optional. The server omits keys whose value is empty, so String getters may return `null` and numeric getters `0`. Sparse records are normal and should not be treated as an error.
</Note>

### Reading the Transcript Content

`transcriptUrl` points at the transcript file. Download it with your HTTP client of choice to read the actual utterances:

<Tabs>
  <Tab title="Kotlin">
    ```kotlin theme={null}
    transcriptRequest.fetchNext(object : CometChatCalls.CallbackListener<List<Transcript>>() {
        override fun onSuccess(transcripts: List<Transcript>) {
            val transcriptUrl = transcripts.firstOrNull()?.transcriptUrl
            if (transcriptUrl != null) {
                // Download the JSON at transcriptUrl using your HTTP client
            }
        }

        override fun onError(e: CometChatException) {
            Log.e(TAG, "Error: ${e.message}")
        }
    })
    ```
  </Tab>

  <Tab title="Java">
    ```java theme={null}
    transcriptRequest.fetchNext(new CometChatCalls.CallbackListener<List<Transcript>>() {
        @Override
        public void onSuccess(List<Transcript> transcripts) {
            if (!transcripts.isEmpty() && transcripts.get(0).getTranscriptUrl() != null) {
                String transcriptUrl = transcripts.get(0).getTranscriptUrl();
                // Download the JSON at transcriptUrl using your HTTP client
            }
        }

        @Override
        public void onError(CometChatException e) {
            Log.e(TAG, "Error: " + e.getMessage());
        }
    });
    ```
  </Tab>
</Tabs>

### Error Handling

All errors — including pre-flight validation — are delivered to `onError()` with a `CometChatException` carrying a `code`. `build()` never throws.

| Condition                                | Code                                                           |
| ---------------------------------------- | -------------------------------------------------------------- |
| `init()` was not called                  | `ERROR_COMETCHAT_CALLS_SDK_INIT`                               |
| No logged-in user (no auth token)        | `ERROR_AUTH_TOKEN`                                             |
| `sessionId` missing or empty             | `ERROR_INVALID_SESSIONID`                                      |
| `limit` is `0` or negative               | `ERROR_NON_POSITIVE_LIMIT`                                     |
| A fetch is already in flight             | `ERROR_REQUEST_IN_PROGRESS`                                    |
| Missing or malformed response            | `ERROR_JSON_EXCEPTION`                                         |
| Network failure or server-side API error | The network or server's own code (e.g. `AUTH_ERR_EMPTY_APPID`) |

## Complete Example

<Tabs>
  <Tab title="Kotlin">
    ```kotlin theme={null}
    // 1. Join a session with transcription enabled
    val sessionSettings = CometChatCalls.SessionSettingsBuilder()
        .setType(SessionType.VIDEO)
        .enableAutoStartTranscription(true)
        .hideTranscriptionButton(false)
        .hideClosedCaptionButton(false)
        .setCaptionLanguage("en-US")
        .build()

    CometChatCalls.joinSession(callToken, sessionSettings, callViewContainer,
        object : CometChatCalls.CallbackListener<CallSession>() {
        override fun onSuccess(callSession: CallSession) {
            // 2. Control transcription during the call
            callSession.startTranscription()
            callSession.stopTranscription()
        }

        override fun onError(e: CometChatException) {
            Log.e(TAG, "Error: ${e.message}")
        }
    })

    // 3. Retrieve transcripts after the call
    val transcriptRequest = TranscriptRequest.TranscriptRequestBuilder()
        .setSessionId(sessionId)
        .setLimit(10)
        .build()

    transcriptRequest.fetchNext(object : CometChatCalls.CallbackListener<List<Transcript>>() {
        override fun onSuccess(transcripts: List<Transcript>) {
            transcripts.forEach { Log.d(TAG, "Transcript URL: ${it.transcriptUrl}") }
        }

        override fun onError(e: CometChatException) {
            Log.e(TAG, "Error: ${e.message}")
        }
    })
    ```
  </Tab>

  <Tab title="Java">
    ```java theme={null}
    // 1. Join a session with transcription enabled
    SessionSettings sessionSettings = new CometChatCalls.SessionSettingsBuilder()
        .setType(SessionType.VIDEO)
        .enableAutoStartTranscription(true)
        .hideTranscriptionButton(false)
        .hideClosedCaptionButton(false)
        .setCaptionLanguage("en-US")
        .build();

    CometChatCalls.joinSession(callToken, sessionSettings, callViewContainer,
        new CometChatCalls.CallbackListener<CallSession>() {
        @Override
        public void onSuccess(CallSession callSession) {
            // 2. Control transcription during the call
            callSession.startTranscription();
            callSession.stopTranscription();
        }

        @Override
        public void onError(CometChatException e) {
            Log.e(TAG, "Error: " + e.getMessage());
        }
    });

    // 3. Retrieve transcripts after the call
    TranscriptRequest transcriptRequest = new TranscriptRequest.TranscriptRequestBuilder()
        .setSessionId(sessionId)
        .setLimit(10)
        .build();

    transcriptRequest.fetchNext(new CometChatCalls.CallbackListener<List<Transcript>>() {
        @Override
        public void onSuccess(List<Transcript> transcripts) {
            for (Transcript transcript : transcripts) {
                Log.d(TAG, "Transcript URL: " + transcript.getTranscriptUrl());
            }
        }

        @Override
        public void onError(CometChatException e) {
            Log.e(TAG, "Error: " + e.getMessage());
        }
    });
    ```
  </Tab>
</Tabs>

## Related Documentation

* [Session Settings](/docs/calls/android/session-settings)
* [Actions](/docs/calls/android/actions)
* [Call Logs](/docs/calls/android/call-logs)
