Recherche avancée

Médias (91)

Autres articles (65)

  • Diogene : création de masques spécifiques de formulaires d’édition de contenus

    26 octobre 2010, par

    Diogene est un des plugins ? SPIP activé par défaut (extension) lors de l’initialisation de MediaSPIP.
    A quoi sert ce plugin
    Création de masques de formulaires
    Le plugin Diogène permet de créer des masques de formulaires spécifiques par secteur sur les trois objets spécifiques SPIP que sont : les articles ; les rubriques ; les sites
    Il permet ainsi de définir en fonction d’un secteur particulier, un masque de formulaire par objet, ajoutant ou enlevant ainsi des champs afin de rendre le formulaire (...)

  • MediaSPIP version 0.1 Beta

    16 avril 2011, par

    MediaSPIP 0.1 beta est la première version de MediaSPIP décrétée comme "utilisable".
    Le fichier zip ici présent contient uniquement les sources de MediaSPIP en version standalone.
    Pour avoir une installation fonctionnelle, il est nécessaire d’installer manuellement l’ensemble des dépendances logicielles sur le serveur.
    Si vous souhaitez utiliser cette archive pour une installation en mode ferme, il vous faudra également procéder à d’autres modifications (...)

  • Utilisation et configuration du script

    19 janvier 2011, par

    Informations spécifiques à la distribution Debian
    Si vous utilisez cette distribution, vous devrez activer les dépôts "debian-multimedia" comme expliqué ici :
    Depuis la version 0.3.1 du script, le dépôt peut être automatiquement activé à la suite d’une question.
    Récupération du script
    Le script d’installation peut être récupéré de deux manières différentes.
    Via svn en utilisant la commande pour récupérer le code source à jour :
    svn co (...)

Sur d’autres sites (5021)

  • HLS script has been lost to time, previous content was made in specific format, attempting to recreate using FFMPEG primitives

    28 février, par Wungo

    Looking to add this video to a stitched playlist. The variants, encoding, and everything must match exactly. We have no access to how things were done previously, so I am literally vibing through this as best as I can.

    


    I recommend using a clip of buck bunny that's 30 seconds long, or the original buck bunny 1080p video.

    


    #!/bin/bash
ffmpeg -i bbb_30s.mp4 -filter_complex "
[0:v]split=7[v1][v2][v3][v4][v5][v6][v7];
[v1]scale=416:234[v1out];
[v2]scale=416:234[v2out];
[v3]scale=640:360[v3out];
[v4]scale=768:432[v4out];
[v5]scale=960:540[v5out];
[v6]scale=1280:720[v6out];
[v7]scale=1920:1080[v7out]
" \
-map "[v1out]" -c:v:0 libx264 -b:v:0 200k -maxrate 361k -bufsize 400k -r 29.97 -g 60 -keyint_min 60 -sc_threshold 0 -preset veryfast -profile:v baseline -level 3.0 \
-map "[v2out]" -c:v:1 libx264 -b:v:1 500k -maxrate 677k -bufsize 700k -r 29.97 -g 60 -keyint_min 60 -sc_threshold 0 -preset veryfast -profile:v baseline -level 3.0 \
-map "[v3out]" -c:v:2 libx264 -b:v:2 1000k -maxrate 1203k -bufsize 1300k -r 29.97 -g 60 -keyint_min 60 -sc_threshold 0 -preset veryfast -profile:v main -level 3.1 \
-map "[v4out]" -c:v:3 libx264 -b:v:3 1800k -maxrate 2057k -bufsize 2200k -r 29.97 -g 60 -keyint_min 60 -sc_threshold 0 -preset veryfast -profile:v main -level 3.1 \
-map "[v5out]" -c:v:4 libx264 -b:v:4 2500k -maxrate 2825k -bufsize 3000k -r 29.97 -g 60 -keyint_min 60 -sc_threshold 0 -preset veryfast -profile:v main -level 4.0 \
-map "[v6out]" -c:v:5 libx264 -b:v:5 5000k -maxrate 5525k -bufsize 6000k -r 29.97 -g 60 -keyint_min 60 -sc_threshold 0 -preset veryfast -profile:v high -level 4.1 \
-map "[v7out]" -c:v:6 libx264 -b:v:6 8000k -maxrate 9052k -bufsize 10000k -r 29.97 -g 60 -keyint_min 60 -sc_threshold 0 -preset veryfast -profile:v high -level 4.2 \
-map a:0 -c:a:0 aac -b:a:0 128k -ar 48000 -ac 2 \
-f hls -hls_time 6 -hls_playlist_type vod -hls_flags independent_segments \
-hls_segment_type fmp4 \
-hls_segment_filename "output_%v_%03d.mp4" \
-master_pl_name master.m3u8 \
-var_stream_map "v:0,name:layer-416x234-200k v:1,name:layer-416x234-500k v:2,name:layer-640x360-1000k v:3,name:layer-768x432-1800k v:4,name:layer-960x540-2500k v:5,name:layer-1280x720-5000k v:6,name:layer-1920x1080-8000k a:0,name:layer-audio-128k" \
output_%v.m3u8



    


    Above is what i've put together over the past few days.

    


    I consistently run into the same issues :

    


      

    1. my variants must match identically, the bit rate etc. must match identically no excuses. No variance allowed.
    2. 


    3. When I did it a different way previously, it became impossible to sync the variants timing, thus making the project not stitchable, making the asset useless.The variants are encoded to last longer than the master.m3u8 says it will last. Rejecting the asset downstream.
    4. 


    5. I end up either having variants mismatched with timing, or no audio/audio channels synced properly. Here is what the master.m3u8 should look like.
    6. 


    


    #EXTM3U
#EXT-X-VERSION:7

#EXT-X-STREAM-INF:AUDIO="aac",AVERAGE-BANDWIDTH=333000,BANDWIDTH=361000,CLOSED-CAPTIONS="cc1",CODECS="avc1.4d400d,mp4a.40.2",FRAME-RATE=29.97,RESOLUTION=416x234
placeholder.m3u8
#EXT-X-STREAM-INF:AUDIO="aac",AVERAGE-BANDWIDTH=632000,BANDWIDTH=677000,CLOSED-CAPTIONS="cc1",CODECS="avc1.4d400d,mp4a.40.2",FRAME-RATE=29.97,RESOLUTION=416x234
placeholder2.m3u8
#EXT-X-STREAM-INF:AUDIO="aac",AVERAGE-BANDWIDTH=1133000,BANDWIDTH=1203000,CLOSED-CAPTIONS="cc1",CODECS="avc1.4d401e,mp4a.40.2",FRAME-RATE=29.97,RESOLUTION=640x360
placeholder3.m3u8

#EXT-X-STREAM-INF:AUDIO="aac",AVERAGE-BANDWIDTH=1933000,BANDWIDTH=2057000,CLOSED-CAPTIONS="cc1",CODECS="avc1.4d401f,mp4a.40.2",FRAME-RATE=29.97,RESOLUTION=768x432
placeholder4.m3u8

#EXT-X-STREAM-INF:AUDIO="aac",AVERAGE-BANDWIDTH=2633000,BANDWIDTH=2825000,CLOSED-CAPTIONS="cc1",CODECS="avc1.4d401f,mp4a.40.2",FRAME-RATE=29.97,RESOLUTION=960x540
placeholder5.m3u8

#EXT-X-STREAM-INF:AUDIO="aac",AVERAGE-BANDWIDTH=5134000,BANDWIDTH=5525000,CLOSED-CAPTIONS="cc1",CODECS="avc1.4d401f,mp4a.40.2",FRAME-RATE=29.97,RESOLUTION=1280x720
placeholder6.m3u8

#EXT-X-STREAM-INF:AUDIO="aac",AVERAGE-BANDWIDTH=8135000,BANDWIDTH=9052000,CLOSED-CAPTIONS="cc1",CODECS="avc1.640028,mp4a.40.2",FRAME-RATE=29.97,RESOLUTION=1920x1080
placeholder7.m3u8

#EXT-X-STREAM-INF:AUDIO="aac",AVERAGE-BANDWIDTH=129000,BANDWIDTH=130000,CLOSED-CAPTIONS="cc1",CODECS="mp4a.40.2"
placeholder8.m3u8

#EXT-X-MEDIA:AUTOSELECT=YES,CHANNELS="2",DEFAULT=YES,GROUP-ID="aac",LANGUAGE="en",NAME="English",TYPE=AUDIO,URI="placeholder8.m3u8"
#EXT-X-MEDIA:AUTOSELECT=YES,DEFAULT=YES,GROUP-ID="cc1",INSTREAM-ID="CC1",LANGUAGE="en",NAME="English",TYPE=CLOSED-CAPTIONS


    


    Underlying playlist clips should be *.mp4 not *.m4s or anything like that. Audio must be on a single channel by itself, closed captions are handled by a remote server and aren't a concern.

    


    as mentioned above :

    


      

    1. I have tried transcoding separately and then combining manually or later. Here is an example of that.
    2. 


    


    #!/bin/bash
set -e

# Input file
INPUT_FILE="bbb_30.mp4"

# Output directory
OUTPUT_DIR="hls_output"
mkdir -p "$OUTPUT_DIR"

# First, extract exact duration from master.m3u8 (if it exists)
MASTER_M3U8="master.m3u8"  # Change if needed

echo "Extracting exact duration from the source MP4..."
EXACT_DURATION=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$INPUT_FILE")
echo "Using exact duration: $EXACT_DURATION seconds"
# Create a reference file with exact duration from the source
echo "Creating reference file with exact duration..."
ffmpeg -y -i "$INPUT_FILE" -c copy -t "$EXACT_DURATION" "$OUTPUT_DIR/exact_reference.mp4"

# Calculate exact GOP size for segment alignment (for 6-second segments at 29.97fps)
FPS=29.97
SEGMENT_DURATION=6
GOP_SIZE=$(echo "$FPS * $SEGMENT_DURATION" | bc | awk '{print int($1)}')
echo "Using GOP size of $GOP_SIZE frames for $SEGMENT_DURATION-second segments at $FPS fps"

# Function to encode a variant with exact duration
encode_variant() {
  local resolution="$1"
  local bitrate="$2"
  local maxrate="$3"
  local bufsize="$4"
  local profile="$5"
  local level="$6"
  local audiorate="$7"
  local name_suffix="$8"
  
  echo "Encoding $resolution variant with video bitrate $bitrate kbps and audio bitrate ${audiorate}k..."
  
  # Step 1: Create an intermediate file with exact duration and GOP alignment
  ffmpeg -y -i "$OUTPUT_DIR/exact_reference.mp4" \
    -c:v libx264 -profile:v "$profile" -level "$level" \
    -x264-params "bitrate=$bitrate:vbv-maxrate=$maxrate:vbv-bufsize=$bufsize:keyint=$GOP_SIZE:min-keyint=$GOP_SIZE:no-scenecut=1" \
    -s "$resolution" -r "$FPS" \
    -c:a aac -b:a "${audiorate}k" \
    -vsync cfr -start_at_zero -reset_timestamps 1 \
    -map 0:v:0 -map 0:a:0 \
    -t "$EXACT_DURATION" \
    -force_key_frames "expr:gte(t,n_forced*6)" \
    "$OUTPUT_DIR/temp_${name_suffix}.mp4"
  
  # Step 2: Create HLS segments with exact boundaries from the intermediate file.
  ffmpeg -y -i "$OUTPUT_DIR/temp_${name_suffix}.mp4" \
    -c copy \
    -f hls \
    -hls_time "$SEGMENT_DURATION" \
    -hls_playlist_type vod \
    -hls_segment_filename "$OUTPUT_DIR/layer-${name_suffix}-segment-%03d.mp4" \
    -hls_flags independent_segments+program_date_time+round_durations \
    -hls_list_size 0 \
    "$OUTPUT_DIR/layer-${name_suffix}.m3u8"
  
  # Verify duration
  VARIANT_DURATION=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$OUTPUT_DIR/temp_${name_suffix}.mp4")
  echo "Variant $name_suffix duration: $VARIANT_DURATION (target: $EXACT_DURATION, diff: $(echo "$VARIANT_DURATION - $EXACT_DURATION" | bc))"
  
  # Clean up temporary file
  rm "$OUTPUT_DIR/temp_${name_suffix}.mp4"
}

# Process each variant with exact duration matching
# Format: resolution, bitrate, maxrate, bufsize, profile, level, audio bitrate, name suffix
encode_variant "416x234" "333" "361" "722" "baseline" "3.0" "64" "416x234-200k"
encode_variant "416x234" "632" "677" "1354" "baseline" "3.0" "64" "416x234-500k"
encode_variant "640x360" "1133" "1203" "2406" "main" "3.0" "96" "640x360-1000k"
encode_variant "768x432" "1933" "2057" "4114" "main" "3.1" "96" "768x432-1800k"
encode_variant "960x540" "2633" "2825" "5650" "main" "3.1" "128" "960x540-2500k"
encode_variant "1280x720" "5134" "5525" "11050" "main" "3.1" "128" "1280x720-5000k"
encode_variant "1920x1080" "8135" "9052" "18104" "high" "4.0" "128" "1920x1080-8000k"

# 8. Audio-only variant
echo "Creating audio-only variant..."


# ffmpeg -y -i "$INPUT_FILE" \
#   -vn -map 0:a \
#   -c:a aac -b:a 128k -ac 2 \ 
#   -t "$EXACT_DURATION" \
#   -f hls \
#   -hls_time "$SEGMENT_DURATION" \
#   -hls_playlist_type vod \
#   -hls_flags independent_segments+program_date_time+round_durations \
#   -hls_segment_filename "$OUTPUT_DIR/layer-audio-128k-segment-%03d.ts" \
#   -hls_list_size 0 \
#   "$OUTPUT_DIR/layer-audio-128k.m3u8"

ffmpeg -y -i "$INPUT_FILE" \
  -vn \
  -map 0:a \
  -c:a aac -b:a 128k \
  -t "$EXACT_DURATION" \
  -f hls \
  -hls_time "$SEGMENT_DURATION" \
  -hls_playlist_type vod \
  -hls_segment_type fmp4 \
  -hls_flags independent_segments+program_date_time+round_durations \
  -hls_list_size 0 \
  -hls_segment_filename "$OUTPUT_DIR/layer-audio-128k-segment-%03d.m4s" \
  "$OUTPUT_DIR/layer-audio-128k.m3u8"


# Create master playlist
cat > "$OUTPUT_DIR/master.m3u8" << EOF
#EXTM3U
#EXT-X-VERSION:7
#EXT-X-INDEPENDENT-SEGMENTS

#EXT-X-STREAM-INF:BANDWIDTH=361000,AVERAGE-BANDWIDTH=333000,CODECS="avc1.4d400d,mp4a.40.2",RESOLUTION=416x234,FRAME-RATE=29.97
layer-416x234-200k.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=677000,AVERAGE-BANDWIDTH=632000,CODECS="avc1.4d400d,mp4a.40.2",RESOLUTION=416x234,FRAME-RATE=29.97
layer-416x234-500k.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=1203000,AVERAGE-BANDWIDTH=1133000,CODECS="avc1.4d401e,mp4a.40.2",RESOLUTION=640x360,FRAME-RATE=29.97
layer-640x360-1000k.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=2057000,AVERAGE-BANDWIDTH=1933000,CODECS="avc1.4d401f,mp4a.40.2",RESOLUTION=768x432,FRAME-RATE=29.97
layer-768x432-1800k.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=2825000,AVERAGE-BANDWIDTH=2633000,CODECS="avc1.4d401f,mp4a.40.2",RESOLUTION=960x540,FRAME-RATE=29.97
layer-960x540-2500k.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=5525000,AVERAGE-BANDWIDTH=5134000,CODECS="avc1.4d401f,mp4a.40.2",RESOLUTION=1280x720,FRAME-RATE=29.97
layer-1280x720-5000k.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=9052000,AVERAGE-BANDWIDTH=8135000,CODECS="avc1.640028,mp4a.40.2",RESOLUTION=1920x1080,FRAME-RATE=29.97
layer-1920x1080-8000k.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=130000,AVERAGE-BANDWIDTH=129000,CODECS="mp4a.40.2"
layer-audio-128k.m3u8
EOF

# Verify all durations match
cat > "$OUTPUT_DIR/verify_all.sh" << 'EOF'
#!/bin/bash

# Get exact reference duration from the exact reference file
REFERENCE_DURATION=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "exact_reference.mp4")
echo "Reference duration: $REFERENCE_DURATION seconds"

# Check each segment's duration
echo -e "\nChecking individual segments..."
for seg in layer-*-segment-*.mp4 layer-audio-128k-segment-*.ts; do
  dur=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$seg")
  echo "$seg: $dur seconds"
done

# Get total duration for each variant by summing segment EXTINF durations from each playlist
echo -e "\nChecking combined variant durations..."
for variant in layer-*.m3u8; do
  total=0
  while read -r line; do
    if [[ $line == "#EXTINF:"* ]]; then
      dur=$(echo "$line" | sed 's/#EXTINF:\([0-9.]*\).*/\1/')
      total=$(echo "$total + $dur" | bc)
    fi
  done < "$variant"
  echo "$variant: $total seconds (reference: $REFERENCE_DURATION, diff: $(echo "$total - $REFERENCE_DURATION" | bc))"
done
EOF

chmod +x "$OUTPUT_DIR/verify_all.sh"

echo "HLS packaging complete with exact duration matching."
echo "Master playlist available at: $OUTPUT_DIR/master.m3u8"
echo "Run $OUTPUT_DIR/verify_all.sh to verify durations."
rm "$OUTPUT_DIR/exact_reference.mp4"



    


    I end up with weird audio in vlc which can't be right, and I also end up with variants being longer than the master.m3u8 playlist which is wonky.

    


    I tried using AI to fix the audio sync issue, and honestly I'm more confused than when I started.

    


  • How to stream synchronized video and audio in real-time from an Android smartphone using HLS while preserving orientation metadata ?

    6 mars, par Jérôme LAROSE
    Hello,  
I am working on an Android application where I need to stream video
from one or two cameras on my smartphone, along with audio from the
microphone, in real-time via a link or web page accessible to users.
The stream should be live, allow rewinding (DVR functionality), and be
recorded simultaneously. A latency of 1 to 2 minutes is acceptable,
and the streaming is one-way.  

I have chosen HLS (HTTP Live Streaming) for its browser compatibility
and DVR support. However, I am encountering issues with audio-video
synchronization, managing camera orientation metadata, and format
conversions.


    


    Here are my attempts :

    


      

    1. MP4 segmentation with MediaRecorder

      


        

      • I used MediaRecorder with setNextOutputFile to generate short MP4 segments, then ffmpeg-kit to convert them to fMP4 for HLS.
      • 


      • Expected : Well-aligned segments for smooth HLS playback.
      • 


      • Result : Timestamp issues causing jumps or interruptions in playback.
      • 


      


    2. 


    3. MPEG2-TS via local socket

      


        

      • I configured MediaRecorder to produce an MPEG2-TS stream sent via a local socket to ffmpeg-kit.
      • 


      • Expected : Stable streaming with preserved metadata.
      • 


      • Result : Streaming works, but orientation metadata is lost, leading to incorrectly oriented video (e.g., rotated 90°).
      • 


      


    4. 


    5. Orientation correction with ffmpeg

      


        

      • I tested -vf transpose=1 in ffmpeg to correct the orientation.
      • 


      • Expected : Correctly oriented video without excessive latency.
      • 


      • Result : Re-encoding takes too long for real-time streaming, causing unacceptable latency.
      • 


      


    6. 


    7. MPEG2-TS to fMP4 conversion

      


        

      • I converted the MPEG2-TS stream to fMP4 with ffmpeg to preserve orientation.
      • 


      • Expected : Perfect audio-video synchronization.
      • 


      • Result : Slight desynchronization between audio and video, affecting the user experience.
      • 


      


    8. 


    


    I am looking for a solution to :

    


      

    • Stream an HLS feed from Android with correctly timestamped segments.
    • 


    • Preserve orientation metadata without heavy re-encoding.
    • 


    • Ensure perfect audio-video synchronization.
    • 


    


    UPDATE

    


    package com.example.angegardien

import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.graphics.SurfaceTexture
import android.hardware.camera2.*
import android.media.*
import android.os.*
import android.util.Log
import android.view.Surface
import android.view.TextureView
import android.view.WindowManager
import androidx.activity.ComponentActivity
import androidx.core.app.ActivityCompat
import com.arthenica.ffmpegkit.FFmpegKit
import fi.iki.elonen.NanoHTTPD
import kotlinx.coroutines.*
import java.io.File
import java.io.IOException
import java.net.ServerSocket
import android.view.OrientationEventListener

/**
 * MainActivity class:
 * - Manages camera operations using the Camera2 API.
 * - Records video using MediaRecorder.
 * - Pipes data to FFmpeg to generate HLS segments.
 * - Hosts a local HLS server using NanoHTTPD to serve the generated HLS content.
 */
class MainActivity : ComponentActivity() {

    // TextureView used for displaying the camera preview.
    private lateinit var textureView: TextureView
    // Camera device instance.
    private lateinit var cameraDevice: CameraDevice
    // Camera capture session for managing capture requests.
    private lateinit var cameraCaptureSession: CameraCaptureSession
    // CameraManager to access camera devices.
    private lateinit var cameraManager: CameraManager
    // Directory where HLS output files will be stored.
    private lateinit var hlsDir: File
    // Instance of the HLS server.
    private lateinit var hlsServer: HlsServer

    // Camera id ("1" corresponds to the rear camera).
    private val cameraId = "1"
    // Flag indicating whether recording is currently active.
    private var isRecording = false

    // MediaRecorder used for capturing audio and video.
    private lateinit var activeRecorder: MediaRecorder
    // Surface for the camera preview.
    private lateinit var previewSurface: Surface
    // Surface provided by MediaRecorder for recording.
    private lateinit var recorderSurface: Surface

    // Port for the FFmpeg local socket connection.
    private val ffmpegPort = 8080

    // Coroutine scope to manage asynchronous tasks.
    private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())

    // Variables to track current device rotation and listen for orientation changes.
    private var currentRotation = 0
    private lateinit var orientationListener: OrientationEventListener

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

        // Initialize the TextureView and set it as the content view.
        textureView = TextureView(this)
        setContentView(textureView)

        // Get the CameraManager system service.
        cameraManager = getSystemService(CAMERA_SERVICE) as CameraManager
        // Setup the directory for HLS output.
        setupHLSDirectory()

        // Start the local HLS server on port 8081.
        hlsServer = HlsServer(8081, hlsDir, this)
        try {
            hlsServer.start()
            Log.d("HLS_SERVER", "HLS Server started on port 8081")
        } catch (e: IOException) {
            Log.e("HLS_SERVER", "Error starting HLS Server", e)
        }

        // Initialize the current rotation.
        currentRotation = getDeviceRotation()

        // Add a listener to detect orientation changes.
        orientationListener = object : OrientationEventListener(this) {
            override fun onOrientationChanged(orientation: Int) {
                if (orientation == ORIENTATION_UNKNOWN) return // Skip unknown orientations.
                // Determine the new rotation angle.
                val newRotation = when {
                    orientation >= 315 || orientation < 45 -> 0
                    orientation >= 45 && orientation < 135 -> 90
                    orientation >= 135 && orientation < 225 -> 180
                    orientation >= 225 && orientation < 315 -> 270
                    else -> 0
                }
                // If the rotation has changed and recording is active, update the rotation.
                if (newRotation != currentRotation && isRecording) {
                    Log.d("ROTATION", "Orientation change detected: $newRotation")
                    currentRotation = newRotation
                }
            }
        }
        orientationListener.enable()

        // Set up the TextureView listener to know when the surface is available.
        textureView.surfaceTextureListener = object : TextureView.SurfaceTextureListener {
            override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) {
                // Open the camera when the texture becomes available.
                openCamera()
            }
            override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) {}
            override fun onSurfaceTextureDestroyed(surface: SurfaceTexture) = false
            override fun onSurfaceTextureUpdated(surface: SurfaceTexture) {}
        }
    }

    /**
     * Sets up the HLS directory in the public Downloads folder.
     * If the directory exists, it deletes it recursively and creates a new one.
     */
    private fun setupHLSDirectory() {
        val downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
        hlsDir = File(downloadsDir, "HLS_Output")

        if (hlsDir.exists()) {
            hlsDir.deleteRecursively()
        }
        hlsDir.mkdirs()

        Log.d("HLS", "📂 HLS folder created: ${hlsDir.absolutePath}")
    }

    /**
     * Opens the camera after checking for necessary permissions.
     */
    private fun openCamera() {
        if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED ||
            ActivityCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
            // Request permissions if they are not already granted.
            ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO), 101)
            return
        }

        try {
            // Open the specified camera using its cameraId.
            cameraManager.openCamera(cameraId, object : CameraDevice.StateCallback() {
                override fun onOpened(camera: CameraDevice) {
                    cameraDevice = camera
                    // Start the recording session once the camera is opened.
                    startNextRecording()
                }
                override fun onDisconnected(camera: CameraDevice) { camera.close() }
                override fun onError(camera: CameraDevice, error: Int) { camera.close() }
            }, null)
        } catch (e: CameraAccessException) {
            e.printStackTrace()
        }
    }

    /**
     * Starts a new recording session:
     * - Sets up the preview and recorder surfaces.
     * - Creates a pipe for MediaRecorder output.
     * - Creates a capture session for simultaneous preview and recording.
     */
    private fun startNextRecording() {
        // Get the SurfaceTexture from the TextureView and set its default buffer size.
        val texture = textureView.surfaceTexture!!
        texture.setDefaultBufferSize(1920, 1080)
        // Create the preview surface.
        previewSurface = Surface(texture)

        // Create and configure the MediaRecorder.
        activeRecorder = createMediaRecorder()

        // Create a pipe to route MediaRecorder data.
        val pipe = ParcelFileDescriptor.createPipe()
        val pfdWrite = pipe[1] // Write end used by MediaRecorder.
        val pfdRead = pipe[0]  // Read end used by the local socket server.

        // Set MediaRecorder output to the file descriptor of the write end.
        activeRecorder.setOutputFile(pfdWrite.fileDescriptor)
        setupMediaRecorder(activeRecorder)
        // Obtain the recorder surface from MediaRecorder.
        recorderSurface = activeRecorder.surface

        // Create a capture request using the RECORD template.
        val captureRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD)
        captureRequestBuilder.addTarget(previewSurface)
        captureRequestBuilder.addTarget(recorderSurface)

        // Create a capture session including both preview and recorder surfaces.
        cameraDevice.createCaptureSession(
            listOf(previewSurface, recorderSurface),
            object : CameraCaptureSession.StateCallback() {
                override fun onConfigured(session: CameraCaptureSession) {
                    cameraCaptureSession = session
                    captureRequestBuilder.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO)
                    // Start a continuous capture request.
                    cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null)

                    // Launch a coroutine to start FFmpeg and MediaRecorder with synchronization.
                    scope.launch {
                        startFFmpeg()
                        delay(500) // Wait for FFmpeg to be ready.
                        activeRecorder.start()
                        isRecording = true
                        Log.d("HLS", "🎥 Recording started...")
                    }

                    // Launch a coroutine to run the local socket server to forward data.
                    scope.launch {
                        startLocalSocketServer(pfdRead)
                    }
                }
                override fun onConfigureFailed(session: CameraCaptureSession) {
                    Log.e("Camera2", "❌ Configuration failed")
                }
            },
            null
        )
    }

    /**
     * Coroutine to start a local socket server.
     * It reads from the MediaRecorder pipe and sends the data to FFmpeg.
     */
    private suspend fun startLocalSocketServer(pfdRead: ParcelFileDescriptor) {
        withContext(Dispatchers.IO) {
            val serverSocket = ServerSocket(ffmpegPort)
            Log.d("HLS", "Local socket server started on port $ffmpegPort")

            // Accept connection from FFmpeg.
            val socket = serverSocket.accept()
            Log.d("HLS", "Connection accepted from FFmpeg")

            // Read data from the pipe and forward it through the socket.
            val inputStream = ParcelFileDescriptor.AutoCloseInputStream(pfdRead)
            val outputStream = socket.getOutputStream()
            val buffer = ByteArray(8192)
            var bytesRead: Int
            while (inputStream.read(buffer).also { bytesRead = it } != -1) {
                outputStream.write(buffer, 0, bytesRead)
            }
            outputStream.close()
            inputStream.close()
            socket.close()
            serverSocket.close()
        }
    }

    /**
     * Coroutine to start FFmpeg using a local TCP input.
     * Applies a video rotation filter based on device orientation and generates HLS segments.
     */
    private suspend fun startFFmpeg() {
        withContext(Dispatchers.IO) {
            // Retrieve the appropriate transpose filter based on current rotation.
            val transposeFilter = getTransposeFilter(currentRotation)

            // FFmpeg command to read from the TCP socket and generate an HLS stream.
            // Two alternative commands are commented below.
            // val ffmpegCommand = "-fflags +genpts -i tcp://localhost:$ffmpegPort -c copy -bsf:a aac_adtstoasc -movflags +faststart -f dash -seg_duration 10 -hls_playlist 1 ${hlsDir.absolutePath}/manifest.mpd"
            // val ffmpegCommand = "-fflags +genpts -i tcp://localhost:$ffmpegPort -c copy -bsf:a aac_adtstoasc -movflags +faststart -f hls -hls_time 5 -hls_segment_type fmp4 -hls_flags split_by_time -hls_list_size 0 -hls_playlist_type event -hls_fmp4_init_filename init.mp4 -hls_segment_filename ${hlsDir.absolutePath}/segment_%03d.m4s ${hlsDir.absolutePath}/playlist.m3u8"
            val ffmpegCommand = "-fflags +genpts -i tcp://localhost:$ffmpegPort -vf $transposeFilter -c:v libx264 -preset ultrafast -crf 23 -c:a copy -movflags +faststart -f hls -hls_time 0.1 -hls_segment_type mpegts -hls_flags split_by_time -hls_list_size 0 -hls_playlist_type event -hls_segment_filename ${hlsDir.absolutePath}/segment_%03d.ts ${hlsDir.absolutePath}/playlist.m3u8"

            FFmpegKit.executeAsync(ffmpegCommand) { session ->
                if (session.returnCode.isValueSuccess) {
                    Log.d("HLS", "✅ HLS generated successfully")
                } else {
                    Log.e("FFmpeg", "❌ Error generating HLS: ${session.allLogsAsString}")
                }
            }
        }
    }

    /**
     * Gets the current device rotation using the WindowManager.
     */
    private fun getDeviceRotation(): Int {
        val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
        return when (windowManager.defaultDisplay.rotation) {
            Surface.ROTATION_0 -> 0
            Surface.ROTATION_90 -> 90
            Surface.ROTATION_180 -> 180
            Surface.ROTATION_270 -> 270
            else -> 0
        }
    }

    /**
     * Returns the FFmpeg transpose filter based on the rotation angle.
     * Used to rotate the video stream accordingly.
     */
    private fun getTransposeFilter(rotation: Int): String {
        return when (rotation) {
            90 -> "transpose=1" // 90° clockwise
            180 -> "transpose=2,transpose=2" // 180° rotation
            270 -> "transpose=2" // 90° counter-clockwise
            else -> "transpose=0" // No rotation
        }
    }

    /**
     * Creates and configures a MediaRecorder instance.
     * Sets up audio and video sources, formats, encoders, and bitrates.
     */
    private fun createMediaRecorder(): MediaRecorder {
        return MediaRecorder().apply {
            setAudioSource(MediaRecorder.AudioSource.MIC)
            setVideoSource(MediaRecorder.VideoSource.SURFACE)
            setOutputFormat(MediaRecorder.OutputFormat.MPEG_2_TS)
            setVideoEncodingBitRate(5000000)
            setVideoFrameRate(24)
            setVideoSize(1080, 720)
            setVideoEncoder(MediaRecorder.VideoEncoder.H264)
            setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
            setAudioSamplingRate(16000)
            setAudioEncodingBitRate(96000) // 96 kbps
        }
    }

    /**
     * Prepares the MediaRecorder and logs the outcome.
     */
    private fun setupMediaRecorder(recorder: MediaRecorder) {
        try {
            recorder.prepare()
            Log.d("HLS", "✅ MediaRecorder prepared")
        } catch (e: IOException) {
            Log.e("HLS", "❌ Error preparing MediaRecorder", e)
        }
    }

    /**
     * Custom HLS server class extending NanoHTTPD.
     * Serves HLS segments and playlists from the designated HLS directory.
     */
    private inner class HlsServer(port: Int, private val hlsDir: File, private val context: Context) : NanoHTTPD(port) {
        override fun serve(session: IHTTPSession): Response {
            val uri = session.uri.trimStart('/')

            // Intercept the request for `init.mp4` and serve it from assets.
            /*
            if (uri == "init.mp4") {
                Log.d("HLS Server", "📡 Intercepting init.mp4, sending file from assets...")
                return try {
                    val assetManager = context.assets
                    val inputStream = assetManager.open("init.mp4")
                    newFixedLengthResponse(Response.Status.OK, "video/mp4", inputStream, inputStream.available().toLong())
                } catch (e: Exception) {
                    Log.e("HLS Server", "❌ Error reading init.mp4 from assets: ${e.message}")
                    newFixedLengthResponse(Response.Status.INTERNAL_ERROR, MIME_PLAINTEXT, "Server error")
                }
            }
            */

            // Serve all other HLS files normally from the hlsDir.
            val file = File(hlsDir, uri)
            return if (file.exists()) {
                newFixedLengthResponse(Response.Status.OK, getMimeTypeForFile(uri), file.inputStream(), file.length())
            } else {
                newFixedLengthResponse(Response.Status.NOT_FOUND, MIME_PLAINTEXT, "File not found")
            }
        }
    }

    /**
     * Clean up resources when the activity is destroyed.
     * Stops recording, releases the camera, cancels coroutines, and stops the HLS server.
     */
    override fun onDestroy() {
        super.onDestroy()
        if (isRecording) {
            activeRecorder.stop()
            activeRecorder.release()
        }
        cameraDevice.close()
        scope.cancel()
        hlsServer.stop()
        orientationListener.disable()
        Log.d("HLS", "🛑 Activity destroyed")
    }
}


    


    I have three examples of ffmpeg commands.

    


      

    • One command segments into DASH, but the camera does not have the correct rotation.
    • 


    • One command segments into HLS without re-encoding with 5-second segments ; it’s fast but does not have the correct rotation.
    • 


    • One command segments into HLS with re-encoding, which applies a rotation. It’s too slow for 5-second segments, so a 1-second segment was chosen.
    • 


    


    Note :

    


      

    • In the second command ("One command segments into HLS without re-encoding with 5-second segments ; it’s fast but does not have the correct rotation."), it returns fMP4. To achieve the correct rotation, I provide a preconfigured init.mp4 file during the HTTP request to retrieve it (see comment).
    • 


    • In the third command ("One command segments into HLS with re-encoding, which applies a rotation. It’s too slow for 5-second segments, so a 1-second segment was chosen."), it returns TS.
    • 


    


  • How to extract frames at 30 fps using FFMPEG APIs on Android ?

    8 septembre 2016, par Amber Beriwal

    We are working on a project that consumes FFMPEG library for video frame extraction on Android platform.

    On Windows, we have observed :

    • Using CLI, ffmpeg is capable of extracting frames at 30 fps using command ffmpeg -i input.flv -vf fps=1 out%d.png.
    • Using Xuggler, we are able to extract frames at 30 fps.
    • Using FFMPEG APIs directly in code, we are getting frames at 30 fps.

    But when we use FFMPEG APIs directly on Android (See Hardware Details), we are getting following results :

    • 720p video (1280 x 720) - 16 fps (approx. 60 ms/frame)
    • 1080p video (1920 x 1080) - 7 fps (approx. 140 ms/frame)

    We haven’t tested Xuggler/CLI on Android yet.

    Ideally, we should be able to get the data in constant time (approx. 30 ms/frame).

    How can we get 30 fps on Android ?

    Code being used on Android :

    if (avformat_open_input(&pFormatCtx, pcVideoFile, NULL, NULL)) {
       iError = -1;  //Couldn't open file
    }

    if (!iError) {
       //Retrieve stream information
       if (avformat_find_stream_info(pFormatCtx, NULL) < 0)
           iError = -2; //Couldn't find stream information
    }

    //Find the first video stream
    if (!iError) {

       for (i = 0; i < pFormatCtx->nb_streams; i++) {
           if (AVMEDIA_TYPE_VIDEO
                   == pFormatCtx->streams[i]->codec->codec_type) {
               iFramesInVideo = pFormatCtx->streams[i]->nb_index_entries;
               duration = pFormatCtx->streams[i]->duration;
               begin = pFormatCtx->streams[i]->start_time;
               time_base = (pFormatCtx->streams[i]->time_base.num * 1.0f)
                       / pFormatCtx->streams[i]->time_base.den;

               pCodecCtx = avcodec_alloc_context3(NULL);
               if (!pCodecCtx) {
                   iError = -6;
                   break;
               }

               AVCodecParameters params = { 0 };
               iReturn = avcodec_parameters_from_context(&params,
                       pFormatCtx->streams[i]->codec);
               if (iReturn < 0) {
                   iError = -7;
                   break;
               }

               iReturn = avcodec_parameters_to_context(pCodecCtx, &params);
               if (iReturn < 0) {
                   iError = -7;
                   break;
               }

               //pCodecCtx = pFormatCtx->streams[i]->codec;

               iVideoStreamIndex = i;
               break;
           }
       }
    }

    if (!iError) {
       if (iVideoStreamIndex == -1) {
           iError = -3; // Didn't find a video stream
       }
    }

    if (!iError) {
       // Find the decoder for the video stream
       pCodec = avcodec_find_decoder(pCodecCtx->codec_id);
       if (pCodec == NULL) {
           iError = -4;
       }
    }

    if (!iError) {
       // Open codec
       if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0)
           iError = -5;
    }

    if (!iError) {
       iNumBytes = av_image_get_buffer_size(AV_PIX_FMT_RGB24, pCodecCtx->width,
               pCodecCtx->height, 1);

       // initialize SWS context for software scaling
       sws_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height,
               pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height,
               AV_PIX_FMT_RGB24,
               SWS_BILINEAR,
               NULL,
               NULL,
               NULL);
       if (!sws_ctx) {
           iError = -7;
       }
    }
    clock_gettime(CLOCK_MONOTONIC_RAW, &end);
    delta_us = (end.tv_sec - start.tv_sec) * 1000000
           + (end.tv_nsec - start.tv_nsec) / 1000;
    start = end;
    //LOGI("Starting_Frame_Extraction: %lld", delta_us);
    if (!iError) {
       while (av_read_frame(pFormatCtx, &packet) == 0) {
           // Is this a packet from the video stream?
           if (packet.stream_index == iVideoStreamIndex) {
               pFrame = av_frame_alloc();
               if (NULL == pFrame) {
                   iError = -8;
                   break;
               }

               // Decode video frame
               avcodec_decode_video2(pCodecCtx, pFrame, &iFrameFinished,
                       &packet);
               if (iFrameFinished) {
                   //OUR CODE
               }
               av_frame_free(&pFrame);
               pFrame = NULL;
           }
           av_packet_unref(&packet);
       }
    }