In the previous tutorial, we created a C program that outputs a sine wave using GStreamer. In this tutorial, we'll take it a step further by creating a Flutter application that utilizes GStreamer to produce a sine wave.
This tutorial is designed for Linux, so we'll be using Linux throughout the process.
Before proceeding with this tutorial, ensure you have the following:
Basic knowledge of the C programming language.
Basic knowledge of Dart/Flutter.
Basic knowledge of CMake.
Basic knowledge of GStreamer.
The instructions in this part of the series are demonstrated on a Linux PC. You can adapt them for other operating systems if needed. Below are the essential tools and software required:
GStreamer Libraries: Install GStreamer and its development libraries. Follow the GStreamer documentation for installation guidelines.
Code Editor: Any code editor will work, but I use VSCode.
CMake: Download from cmake.org. On Linux, you can install it via your package manager.
Flutter: Install Flutter by following the instructions on the official Flutter website.
We'll use the GStreamer pipeline from the previous tutorial:
gst-launch-1.0 audiotestsrc ! audioconvert ! autoaudiosink
We'll structure our project to include a Flutter app and an FFI (Foreign Function Interface) plugin.
Create the project directories and files:
mkdir app
cd app
flutter create fltgst # create app
flutter create -t plugin_ffi native_binding --platforms linux,windows,macos,android,ios # create ffi plugin
code fltgst # open app project in VSCode
code native_binding # open ffi project in VSCode
First, let's modify the native_binding.c
file in the FFI plugin project. Initially, it looks like this:
#include "native_binding.h"
// A very short-lived native function.
//
// For very short-lived functions, it is fine to call them on the main isolate.
// They will block the Dart execution while running the native function, so
// only do this for native functions which are guaranteed to be short-lived.
FFI_PLUGIN_EXPORT int sum(int a, int b) { return a + b; }
// A longer-lived native function, which occupies the thread calling it.
//
// Do not call these kind of native functions in the main isolate. They will
// block Dart execution. This will cause dropped frames in Flutter applications.
// Instead, call these native functions on a separate isolate.
FFI_PLUGIN_EXPORT int sum_long_running(int a, int b) {
// Simulate work.
#if _WIN32
Sleep(5000);
#else
usleep(5000 * 1000);
#endif
return a + b;
}
Next, copy everything except the main
function from main.c
in the previous tutorial and paste it into native_binding.c
. You'll notice the functions in native_binding.c
have a FFI_PLUGIN_EXPORT
macro. This macro marks functions as externally accessible so they can be called from Dart.
Here’s how native_binding.c
looks after modification:
#include <gst/gst.h>
#include "native_binding.h"
// gst-launch-1.0 audiotestsrc ! audioconvert ! autoaudiosink
typedef struct _FltGstData
{
GstElement *pipeline;
GstElement *audiotestsrc;
GstElement *audioconvert;
GstElement *autoaudiosink;
GMainLoop *mainloop;
} FltGstData;
FltGstData *data;
FFI_PLUGIN_EXPORT void init(void)
{
// init
gst_init(NULL, NULL);
data = (FltGstData *)malloc(sizeof(FltGstData));
}
FFI_PLUGIN_EXPORT void setup_pipeline(void)
{
// setup pipeline
data->audiotestsrc = gst_element_factory_make("audiotestsrc", NULL);
data->audioconvert = gst_element_factory_make("audioconvert", NULL);
data->autoaudiosink = gst_element_factory_make("autoaudiosink", NULL);
data->pipeline = gst_pipeline_new(NULL);
data->mainloop = g_main_loop_new(NULL, FALSE);
if (!data->audiotestsrc || !data->audioconvert || !data->autoaudiosink || !data->pipeline)
{
g_printerr("Elements could not be created.\n");
gst_object_unref(data->pipeline);
return;
}
gst_bin_add_many(GST_BIN(data->pipeline), data->audiotestsrc, data->audioconvert, data->autoaudiosink, NULL);
if (!gst_element_link_many(data->audiotestsrc, data->audioconvert, data->autoaudiosink, NULL))
{
g_printerr("Elements could not be created.\n");
gst_object_unref(data->pipeline);
return;
}
}
FFI_PLUGIN_EXPORT void start_pipeline(void)
{
// start pipeline
GstStateChangeReturn ret;
ret = gst_element_set_state(data->pipeline, GST_STATE_PLAYING);
if (ret == GST_STATE_CHANGE_FAILURE)
{
g_printerr("Elements could not be created.\n");
gst_object_unref(data->pipeline);
return;
}
}
FFI_PLUGIN_EXPORT void run_mainloop(void)
{
// run main loop
g_main_loop_run(data->mainloop);
}
FFI_PLUGIN_EXPORT void free_resource(void)
{
// free resources
gst_element_set_state(data->pipeline, GST_STATE_NULL);
gst_object_unref(data->pipeline);
}
// A very short-lived native function.
//
// For very short-lived functions, it is fine to call them on the main isolate.
// They will block the Dart execution while running the native function, so
// only do this for native functions which are guaranteed to be short-lived.
FFI_PLUGIN_EXPORT int sum(int a, int b) { return a + b; }
// A longer-lived native function, which occupies the thread calling it.
//
// Do not call these kind of native functions in the main isolate. They will
// block Dart execution. This will cause dropped frames in Flutter applications.
// Instead, call these native functions on a separate isolate.
FFI_PLUGIN_EXPORT int sum_long_running(int a, int b) {
// Simulate work.
#if _WIN32
Sleep(5000);
#else
usleep(5000 * 1000);
#endif
return a + b;
}
Next, update the header file native_binding.h
with the function definitions and the FFI_PLUGIN_EXPORT
macro:
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#if _WIN32
#include <windows.h>
#else
#include <pthread.h>
#include <unistd.h>
#endif
#if _WIN32
#define FFI_PLUGIN_EXPORT __declspec(dllexport)
#else
#define FFI_PLUGIN_EXPORT
#endif
FFI_PLUGIN_EXPORT void init(void);
FFI_PLUGIN_EXPORT void setup_pipeline(void);
FFI_PLUGIN_EXPORT void start_pipeline(void);
FFI_PLUGIN_EXPORT void run_mainloop(void);
FFI_PLUGIN_EXPORT void free_resource(void);
// A very short-lived native function.
//
// For very short-lived functions, it is fine to call them on the main isolate.
// They will block the Dart execution while running the native function, so
// only do this for native functions which are guaranteed to be short-lived.
FFI_PLUGIN_EXPORT int sum(int a, int b);
// A longer lived native function, which occupies the thread calling it.
//
// Do not call these kind of native functions in the main isolate. They will
// block Dart execution. This will cause dropped frames in Flutter applications.
// Instead, call these native functions on a separate isolate.
FFI_PLUGIN_EXPORT int sum_long_running(int a, int b);
Update the CMakeLists.txt
file in the native_binding/src/
directory to link against GStreamer. Refer to the CMakeLists.txt
from the previous tutorial:
# The Flutter tooling requires that developers have CMake 3.10 or later
# installed. You should not increase this version, as doing so will cause
# the plugin to fail to compile for some customers of the plugin.
cmake_minimum_required(VERSION 3.10)
project(native_binding_library VERSION 0.0.1 LANGUAGES C)
find_package(PkgConfig REQUIRED)
pkg_search_module(GST REQUIRED gstreamer-1.0)
include_directories(${GST_INCLUDE_DIRS})
add_library(native_binding SHARED
"native_binding.c"
)
target_link_libraries(native_binding PRIVATE ${GST_LDFLAGS})
set_target_properties(native_binding PROPERTIES
PUBLIC_HEADER native_binding.h
OUTPUT_NAME "native_binding"
)
target_compile_definitions(native_binding PUBLIC DART_SHARED_LIB)
Generate the FFI files using ffigen
. In the native_binding
directory, run:
dart run ffigen --config ffigen.yaml
This generates function pointers in native_binding/lib/native_binding_bindings_generated.dart
. Map these pointers to Dart functions in native_binding/lib/native_binding.dart
:
void init() => _bindings.init();
void setupPipeline() => _bindings.setup_pipeline();
void startPipeline() => _bindings.start_pipeline();
void runMainloop() => _bindings.run_mainloop();
void freeResource() => _bindings.free_resource();
The work on the native_binding
project is complete. Now, let’s update the app
project.
Add native_binding
as a dependency using a relative path. In app/pubspec.yaml
, add the following under the dependencies
section:
native_binding:
path: ../native_binding
Run flutter pub get
to update the dependencies.
When you build the app, you should see the files in the native_binding
project being compiled. Use the following command to build:
flutter build linux -vvv --debug
You'll notice output similar to:
$ flutter build linux -vvv --debug
...
[ +11 ms] ninja: Entering directory `build/linux/x64/debug'
[ +380 ms] [1/8] Building C object plugins/native_binding/shared/CMakeFiles/native_binding.dir/native_binding.c.o
[ +135 ms] [2/8] Linking C shared library plugins/native_binding/shared/libnative_binding.so
...
Update app/lib/main.dart
to call the functions in native_binding/lib/native_binding.dart
:
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:native_binding/native_binding.dart' as native_binding;
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
void initState() {
super.initState();
native_binding.init();
native_binding.setupPipeline();
native_binding.startPipeline();
}
void dispose() {
native_binding.freeResource();
super.dispose();
}
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return Scaffold(
appBar: AppBar(
// TRY THIS: Try changing the color here to a specific color (to
// Colors.amber, perhaps?) and trigger a hot reload to see the AppBar
// change color while the other colors stay the same.
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title),
),
body: const Center(
child: Text("FltGst"),
),
);
}
}
When you run the app, you should hear a sine wave.
To debug our native code, open VSCode and navigate to the debug tab of the app project, click create a launch.json file
, then add the following section to the configurations
section:
{
"name": "Linux Native",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/build/linux/x64/debug/bundle/fltgst",
"args": [],
"environment": [],
"cwd": "${workspaceFolder}"
},
Then select Linux Native
, head back to the files tab, expand DEPENDENCIES
-> direct dependencies
-> native_binding
, then open native_binding.c
, add a breakpoint at any line, and hit F5
to run the app. You should see the breakpoint being hit.
Find the repository and files updated in this tutorial at:
Repository: https://github.com/fengjiongmax/fltgst
Files after changes: https://github.com/fengjiongmax/fltgst/tree/99ebdc1b811bc5ed9ff36063d4e45d0c8a77d6dc