<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: khomin</title>
    <description>The latest articles on DEV Community by khomin (@khomin).</description>
    <link>https://dev.to/khomin</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F388736%2F12a7f371-2370-4b5e-a749-77bac8795fe0.png</url>
      <title>DEV Community: khomin</title>
      <link>https://dev.to/khomin</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/khomin"/>
    <language>en</language>
    <item>
      <title>Electron Camera(cpp+ffmpeg)</title>
      <dc:creator>khomin</dc:creator>
      <pubDate>Tue, 08 Mar 2022 08:05:39 +0000</pubDate>
      <link>https://dev.to/khomin/electron-cameracppffmpeg-46ba</link>
      <guid>https://dev.to/khomin/electron-cameracppffmpeg-46ba</guid>
      <description>&lt;p&gt;An example of using Electron + React JS and a native ffmpeg addon to access a webcamera&lt;/p&gt;

&lt;p&gt;This guide may be helpful to someone who is trying to find a way&lt;br&gt;to work with Electron if they need to use a c++ library or code&lt;br&gt;&lt;br&gt;
I was looking for a more realistic example than a simple 'hello world' and i didn't succeed&lt;br&gt;&lt;/p&gt;

&lt;p&gt;Here are the links in advance:&lt;br&gt;
&lt;a href="https://github.com/khomin/electron_camera_ffmpeg"&gt;electron - https://github.com/khomin/electron_camera_ffmpeg&lt;/a&gt;&lt;br&gt;
&lt;a href="https://github.com/khomin/electron_ffmpeg_addon_camera"&gt;addon - https://github.com/khomin/electron_ffmpeg_addon_camera&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So let me share my experience&lt;br&gt;&lt;br&gt;
We have three layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;main (launches BrowserWindow, listens for signals and is considered a backend)
&lt;/li&gt;
&lt;li&gt;rendering (runs React JS, handles UI events, shows video frame and info)
&lt;/li&gt;
&lt;li&gt;native (responsible for ffmpeg, starts/stops the video, sends a callback to the main layer)
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Render thread cannot directly access the main thread and vice versa&lt;br&gt;&lt;br&gt;
All communications must be done through the ipcMain/ipcRenderer modules&lt;br&gt;&lt;br&gt;
(it provides methods to allow synchronous and asynchronous messages to be sent from these layers)&lt;br&gt;&lt;/p&gt;

&lt;p&gt;Set the listener in main.ts&lt;br&gt;&lt;br&gt;
It will receive events from Render thread and pass them to native layer&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ipcMain.on('ipc-example', async (event, arg) =&amp;gt; {
  if(arg.type == 'startCamera') {
    addon.setCameraEnabled()
  } else if(arg.type == 'stopCamera') {
    addon.setCameraDisable()
  } else if(arg.type == 'setDimention') {
      addon.setDimention(arg.width, arg.height)
  }
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Listen to responses from Native and translate them to Render thread&lt;br&gt;&lt;br&gt;
We set the listener callback just a couple of lines below&lt;br&gt;&lt;br&gt;
So we have a completed chain:&lt;br&gt;
    Render -&amp;gt; Main -&amp;gt; Native&lt;br&gt;
    Native -&amp;gt; Main -&amp;gt; Render&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  addon.setCb(function(data) {
    if(data.type == 'stats') {
      mainWindow.webContents.send('ipc-example_stats', data)
    } else if(data.type == 'frame') {
      mainWindow.webContents.send('ipc-example_frame', data)
    }
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now it's time to see what's on the render thread&lt;br&gt;&lt;br&gt;
We will send events 'startCamera', 'stopCamera' and 'setDimention&lt;br&gt;&lt;br&gt;
It's a simple React.Component class and props for handling UI logic&lt;br&gt;&lt;br&gt;
I hope everything is clear from the names:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export default class Root extends React.Component {
  constructor(props) {
      super(props);
      this.state = { 
        videoActive: false,
        buttonText: 'Start video',
        packets: 0,
        errors: 0,
        resolution: 0,
        frame: null,
        frameBytes: 0,
        frameWidth: 1000,
        frameHeight: 1000
      };
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To send messages from Render to Main use this construct&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ipcRenderer.&amp;lt;method name&amp;gt;()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And to set a listener on certain channel&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ipcRenderer.on('ipc-name', (cb) =&amp;gt; {}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So, the full code is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  componentDidMount() {
      window.electron.ipcRenderer.on('ipc-example_stats', (data) =&amp;gt; {
        this.setState({ videoActive: data.is_active == true } )
        this.setState({ buttonText: data.is_active == true ? 'stop video' : 'start video'} )
        this.setState({ packets: data.packet_cnt } )
        this.setState({ errors: data.err_cnt } )
      });
      window.electron.ipcRenderer.on('ipc-example_frame', (data) =&amp;gt; {
        this.setState({ resolution: data.width + 'x' + data.height} )
        this.setState({ frame: data.data} )
        this.setState({ frameBytes: data.data.byteLength } )
        this.setState({ frameWidth: data.width } )
        this.setState({ frameHeight: data.height } )
        this.updateFrame()
      });
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And when the class is no longer needed, we have to remove these listeners:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  componentWillUnmount() {
    window.electron.ipcRenderer.removeListener('ipc-example_stats')
    window.electron.ipcRenderer.removeListener('ipc-example_frame')
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We may have noticed the this.updateFrame() method&lt;br&gt;&lt;br&gt;
This is where the canvas is loaded with a video frame:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  updateFrame() {
    var canvas = document.getElementById("frameCanvas");
    var ctx = canvas.getContext("2d");
    var data = this.state.frame
    var len = this.state.frameBytes
    var frameHeight = this.state.frameHeight
    var frameWidth = this.state.frameWidth
    if(data == null || len == 0 || frameHeight == 0 || frameWidth == 0) return

    var imageData = ctx.createImageData(frameWidth, frameHeight);
    const data_img = imageData.data;
    var pixels = new Uint8Array(data)
    var i = 0; // cursor for RGBA buffer
    var t = 0; // cursor for RGB buffer
    var _len = data_img.length
    for(; i &amp;lt; _len; i += 4) {
      data_img[i]   = pixels[t+2]
      data_img[i+1] = pixels[t+1]
      data_img[i+2] = pixels[t]
      data_img[i+3] = 255
      t += 4;
    }
    ctx.putImageData(imageData, 0, 0);
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The UI will look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  render() {
      return (
      &amp;lt;div&amp;gt;
        &amp;lt;Row className="topPanel"&amp;gt;
          &amp;lt;div className="status"/&amp;gt;

          {/*
             button enable/disable video 
          */}
          &amp;lt;Button className="button"
            onClick={()=&amp;gt; {
              if(this.state.videoActive) {
                window.electron.ipcRenderer.stopCamera()
              } else {
                window.electron.ipcRenderer.startCamera()
              }
            }
          }&amp;gt;{this.state.buttonText}&amp;lt;/Button&amp;gt;
        &amp;lt;/Row&amp;gt;

        {/*
             statistics 
        */}
        &amp;lt;Col className="stats"&amp;gt;
            &amp;lt;label className="text_caption"&amp;gt;Packets: {this.state.packets},&amp;lt;/label&amp;gt;
            &amp;lt;label className="text_caption"&amp;gt;Errors: {this.state.errors},&amp;lt;/label&amp;gt;
            &amp;lt;label className="text_caption"&amp;gt;Resolution: {this.state.resolution}&amp;lt;/label&amp;gt;
        &amp;lt;/Col&amp;gt;
        &amp;lt;canvas
          id="frameCanvas"
          width={this.state.frameWidth}
          height={this.state.frameHeight}
      /&amp;gt;
      &amp;lt;/div&amp;gt;
      );
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now let's look at the native layer&lt;br&gt;
Most of the work is in it&lt;br&gt;
First time I thought it would be really hard&lt;br&gt;
Especially concerning linking and compiling libraries&lt;br&gt;&lt;br&gt;
But it turned out to be quite simple, since the 'node-gyb build'&lt;br&gt;
does its job perfectly and there is not much difference compared to the bare cmake&lt;/p&gt;

&lt;p&gt;The entry point is "Init"&lt;br&gt;
In this place we create m_video and set the listeners&lt;br&gt;&lt;br&gt;
We cannot send data to JS right away&lt;br&gt;
V8 imposes restrictions on access to threads&lt;br&gt;
Thus it is impossible to pass data from other thread to main without synchronization&lt;br&gt;
Thread-safe methods called Napi::ThreadSafeFunction are used for this task&lt;br&gt;&lt;br&gt;
The strategy is to store the data from the callback into a queue&lt;br&gt;
And process this queue from Napi::ThreadSafeFunction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Napi::Object Init(Napi::Env env, Napi::Object exports) {
    m_video = new Video();
    m_video-&amp;gt;setStatusCallBack(([&amp;amp;](VideStats stats) {
        if(threadCtx == NULL) return;
        std::lock_guard&amp;lt;std::mutex&amp;gt;lk(threadCtx-&amp;gt;m_data_lock);
        auto data = new DataItemStats();
        data-&amp;gt;type = DataItemType::DataStats;
        data-&amp;gt;stats = new VideStats();
        data-&amp;gt;stats-&amp;gt;is_active = stats.is_active;
        data-&amp;gt;stats-&amp;gt;packet_cnt = stats.packet_cnt;
        data-&amp;gt;stats-&amp;gt;err_cnt = stats.err_cnt;
        threadCtx-&amp;gt;m_data_queue.push(data);
        threadCtx-&amp;gt;m_data_cv.notify_one();
    }));
    m_video-&amp;gt;setFrameCallBack(([&amp;amp;](AVFrame* frame, uint32_t bufSize) {
        if(frame != NULL) {
            std::lock_guard&amp;lt;std::mutex&amp;gt;lk(threadCtx-&amp;gt;m_data_lock);
            auto data = new DataItemFrame();
            data-&amp;gt;type = DataItemType::DataFrame;
            data-&amp;gt;frame = new uint8_t[bufSize];
            data-&amp;gt;frame_buf_size = bufSize;
            data-&amp;gt;width = frame-&amp;gt;width;
            data-&amp;gt;height = frame-&amp;gt;height;
            memcpy(data-&amp;gt;frame, (uint8_t*)frame-&amp;gt;data[0], bufSize);
            threadCtx-&amp;gt;m_data_queue.push(data);
            threadCtx-&amp;gt;m_data_cv.notify_one();
        } else {
            std::cout &amp;lt;&amp;lt; "frameCallback: frame == null" &amp;lt;&amp;lt; std::endl;
        }
    }));
    exports["setCb"] = Napi::Function::New(env, setCallback, std::string("setCallback"));
    exports.Set(Napi::String::New(env, "setCameraEnabled"), Napi::Function::New(env, StartVideo));
    exports.Set(Napi::String::New(env, "setCameraDisable"), Napi::Function::New(env, StopVideo));
    exports.Set(Napi::String::New(env, "setDimention"), Napi::Function::New(env, SetDimention));
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside the queue, use these classes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class DataItem {
public:
    DataItemType type;
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And since we have different data types (frames, info)&lt;br&gt;&lt;br&gt;
The best way is to extend derived classes&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class DataItemStats : public DataItem {
public:
    VideStats* stats;
};
class DataItemFrame : public DataItem {
public:
    uint8_t* frame;
    uint32_t frame_buf_size;
    int width;
    int height;
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All data is collected inside one class for convenience:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;struct ThreadCtx {
    ThreadCtx(Napi::Env env) {};
    std::thread nativeThread;
    Napi::ThreadSafeFunction tsfn;
    bool toCancel = false;
    std::queue&amp;lt;DataItem*&amp;gt; m_data_queue;
    std::mutex m_data_lock;
    std::condition_variable m_data_cv;
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the methods that were described above in - exports["setCb"]&lt;br&gt;&lt;br&gt;
Must have an implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Napi::Value setCallback(const Napi::CallbackInfo&amp;amp; info) {
    auto env = info.Env();
    threadCtx = new ThreadCtx(env);
    // a safe function
    threadCtx-&amp;gt;tsfn = Napi::ThreadSafeFunction::New(
                            env, 
                            info[0].As&amp;lt;Napi::Function&amp;gt;(),
                            "CallbackMethod", 
                            0, 1 , 
                            threadCtx,
        [&amp;amp;]( Napi::Env, void *finalizeData, ThreadCtx *context ) {
            threadCtx-&amp;gt;nativeThread.join();
        },
        (void*)nullptr
    );

    // a thread for the queue
    // it calls threadCtx-&amp;gt;tsfn.BlockingCall
    // and sends a json to js layer
    threadCtx-&amp;gt;nativeThread = std::thread([&amp;amp;]{
        auto callbackStats = [](Napi::Env env, Napi::Function cb, char* buffer) {
            auto data = (DataItemStats*)buffer;
            if(data == NULL) return;

            Napi::Object obj = Napi::Object::New(env);
            obj.Set("type", std::string("stats"));
            obj.Set("is_active", std::to_string(data-&amp;gt;stats-&amp;gt;is_active));
            obj.Set("packet_cnt", std::to_string(data-&amp;gt;stats-&amp;gt;packet_cnt));
            obj.Set("err_cnt", std::to_string(data-&amp;gt;stats-&amp;gt;err_cnt));
            cb.Call({obj});
            delete data-&amp;gt;stats;
            delete data;
        };
        auto callbackFrame = [](Napi::Env env, Napi::Function cb, char* buffer) {
            auto data = (DataItemFrame*)buffer;
            if(data == NULL) return;

            napi_value arrayBuffer;
            void* yourPointer = malloc(data-&amp;gt;frame_buf_size);
            napi_create_arraybuffer(env, data-&amp;gt;frame_buf_size, &amp;amp;yourPointer, &amp;amp;arrayBuffer);
            memcpy((uint8_t*)yourPointer, data-&amp;gt;frame, data-&amp;gt;frame_buf_size);

            Napi::Object obj = Napi::Object::New(env);
            obj.Set("type", std::string("frame"));
            obj.Set("data", arrayBuffer);
            obj.Set("width", data-&amp;gt;width);
            obj.Set("height", data-&amp;gt;height);
            cb.Call({obj});
            delete data-&amp;gt;frame;
            delete data;
        };
        while(!threadCtx-&amp;gt;toCancel) {
            DataItem* data_item = NULL;
            std::unique_lock&amp;lt;std::mutex&amp;gt; lk(threadCtx-&amp;gt;m_data_lock);
            threadCtx-&amp;gt;m_data_cv.wait(lk, [&amp;amp;] {
                return !threadCtx-&amp;gt;m_data_queue.empty();
            });

            while(!threadCtx-&amp;gt;m_data_queue.empty()) {
                data_item = threadCtx-&amp;gt;m_data_queue.front();
                threadCtx-&amp;gt;m_data_queue.pop();
                if(data_item == NULL) continue;

                if(data_item-&amp;gt;type == DataItemType::DataStats) {
                    napi_status status = threadCtx-&amp;gt;tsfn.BlockingCall((char*)data_item, callbackStats);
                    if (status != napi_ok) {
                        // Handle error
                        break;
                    }
                } else if(data_item-&amp;gt;type == DataItemType::DataFrame) {
                    napi_status status = threadCtx-&amp;gt;tsfn.BlockingCall((char*)data_item, callbackFrame);
                    if (status != napi_ok) {
                        // Handle error
                        break;
                    }
                }
            }
        }
        threadCtx-&amp;gt;tsfn.Release();
    });
    return Napi::String::New(info.Env(), std::string("SimpleAsyncWorker for seconds queued.").c_str());
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And a couple of methods that don't need a queue:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Napi::Boolean StartVideo(const Napi::CallbackInfo&amp;amp; info) {
    std::cout &amp;lt;&amp;lt; "Command: startCamera\n";
    if(!m_video-&amp;gt;isStarted()) {
        m_video-&amp;gt;startVideoCamera();
    }
    Napi::Env env = info.Env();
    return Napi::Boolean::New(env, true);
}
Napi::Boolean StopVideo(const Napi::CallbackInfo&amp;amp; info) {
    std::cout &amp;lt;&amp;lt; "Command: stopCamera\n";
    if(m_video-&amp;gt;isStarted()) {
        m_video-&amp;gt;stopVideo();
    }
    Napi::Env env = info.Env();
    return Napi::Boolean::New(env, true);
}
Napi::Value SetDimention(const Napi::CallbackInfo&amp;amp; info) {
    if(m_video == NULL || !m_video-&amp;gt;isStarted()) {
        std::cout &amp;lt;&amp;lt; "Command: setDimention -camera is not started!\n";
    } else if(info.Length() == 2) {
        int width = info[0].As&amp;lt;Napi::Value&amp;gt;().ToNumber();
        int height = info[1].As&amp;lt;Napi::Value&amp;gt;().ToNumber();;
        std::cout &amp;lt;&amp;lt; "Command: setDimention: " &amp;lt;&amp;lt; ",width=" &amp;lt;&amp;lt; width &amp;lt;&amp;lt; ",height=" &amp;lt;&amp;lt; height &amp;lt;&amp;lt; std::endl;
        m_video-&amp;gt;setResolution(width, height);
    } else {
        std::cout &amp;lt;&amp;lt; "Command: setDimention missed arguments\n";
    }
    return Napi::Number::New(info.Env(), true);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At the end should be this define:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NODE_API_MODULE(addon, Init)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The c++ addon itself is included as a submodule and will be cloned automatically&lt;br&gt;&lt;br&gt;
But it has to be built independently&lt;br&gt;&lt;/p&gt;

&lt;p&gt;Keep in mind&lt;br&gt;
The build ffmpeg is not in the repository (because of its relatively large size)&lt;br&gt;
You must build ffmpeg as a shared library&lt;br&gt;
And then edit the path in binding.gyp (src/native/binding.gyp)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;'libraries': [
  '../src/ffmpeg_mac/lib/libavcodec.58.91.100.dylib',
  '../src/ffmpeg_mac/lib/libavdevice.58.10.100.dylib',
  '../src/ffmpeg_mac/lib/libavfilter.7.85.100.dylib',
  '../src/ffmpeg_mac/lib/libavformat.58.45.100.dylib',
  '../src/ffmpeg_mac/lib/libavutil.56.51.100.dylib',
  '../src/ffmpeg_mac/lib/libpostproc.55.7.100.dylib',
  '../src/ffmpeg_mac/lib/libswresample.3.7.100.dylib',
  '../src/ffmpeg_mac/lib/libswscale.5.7.100.dylib',
],
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The project is written on macos&lt;br&gt;
If you need Windows/Linux support, you must specify the appropriate methods for avformat_open_input&lt;br&gt;
You can see the exact location by this code&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const char* VideoSource::getDeviceFamily() {
#ifdef _WIN32
  const char *device_family = "dshow";
#elif __APPLE__
  const char *device_family = "avfoundation";
#elif __linux__
  const char *device_family = "v4l2";
#endif
  return device_family;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you have any question you can contact me over email/mesages&lt;br&gt;&lt;/p&gt;

</description>
      <category>electron</category>
      <category>ffmpeg</category>
      <category>cpp</category>
      <category>addon</category>
    </item>
    <item>
      <title>How to get Open Graph meta tags in QML</title>
      <dc:creator>khomin</dc:creator>
      <pubDate>Fri, 09 Apr 2021 20:26:52 +0000</pubDate>
      <link>https://dev.to/khomin/how-to-get-open-graph-meta-tags-in-qml-4fnb</link>
      <guid>https://dev.to/khomin/how-to-get-open-graph-meta-tags-in-qml-4fnb</guid>
      <description>&lt;p&gt;One day i needed a solution that could parse meta graph tags from a input line and produce a title and an icon&lt;/p&gt;

&lt;p&gt;Of course there were an infinite number of libraries that used jsoup's, but that was not what i needed, i wanted to use qt and c++&lt;br&gt;
I thought as soon i enter my query - "c++ parser meta tags" i will see all the solutions o was looking for&lt;br&gt;
But in reality, everything is a little more complicated. &lt;br&gt;
What i did:&lt;br&gt;
1) Prepare step, parse the input and decide if there is a valid url or just text&lt;br&gt;
This step seems to be expensive (not so much as loading everything from input, &lt;br&gt;
but I think it is too extra work)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nx"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;checkIsContainsHyperlink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;QString&lt;/span&gt; &lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nx"&gt;QRegularExpression&lt;/span&gt; &lt;span class="nf"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;web_pattern&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;QRegularExpressionMatch&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hasMatch&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;2) Download with the ability to handle redirects &lt;br&gt;
Many sites do not provide tags on simple web pages, and they often use redirect for reasons which i don't know&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;m_WebCtrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;SIGNAL &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;finished&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;QNetworkReply&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;SLOT &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;fileDownloaded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;QNetworkReply&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
&lt;span class="nx"&gt;QNetworkRequest&lt;/span&gt; &lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;QNetworkRequest&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nx"&gt;RedirectPolicyAttribute&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;m_WebCtrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;3) Saving the page we downloaded it seems strange, why we save this page is probably surprising you &lt;br&gt;
The problem is that some sites can ban a specific IP, which makes a lot of requests&lt;br&gt;
For me it was enough to change 3-5 symbols in the url line and i got banned for a few minutes&lt;br&gt;
Caching downloaded pages solved this problem&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m_downloader_image&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;FileDownloader&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nx"&gt;downloaded&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;imagePathName&lt;/span&gt;&lt;span class="p"&gt;]()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;QByteArray&lt;/span&gt; &lt;span class="nx"&gt;array&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;m_downloader_image&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;downloadedData&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isEmpty&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;QFile&lt;/span&gt; &lt;span class="nf"&gt;imageFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;imagePathName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;imageFile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;QIODevice&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nx"&gt;WriteOnly&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;imageFile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;array&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nx"&gt;m_result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;og_image_local_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;imagePathName&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;emit&lt;/span&gt; &lt;span class="nf"&gt;signalParserDone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m_result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;4) Parsing&lt;br&gt;
So we have a web-page in the local folder, it's time to parse it and get what we need&lt;br&gt;
Unfortunately, for me, gumbo-parser turned out to be very unfriendly&lt;br&gt;
So for first start i decided to use regex, hoping to change it to something else in the future&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;QRegularExpression&lt;/span&gt; &lt;span class="nf"&gt;site_name_regex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;og_site_name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;QRegularExpression&lt;/span&gt; &lt;span class="nf"&gt;title_regex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;og_title&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;QRegularExpression&lt;/span&gt; &lt;span class="nf"&gt;description_regex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;og_description&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;QRegularExpression&lt;/span&gt; &lt;span class="nf"&gt;url_regex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;og_url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;QRegularExpression&lt;/span&gt; &lt;span class="nf"&gt;image_regex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;og_image&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;QRegularExpressionMatch&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;site_name_regex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hasMatch&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;og_site_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;captured&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;title_regex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hasMatch&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;og_title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;captured&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;description_regex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hasMatch&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;og_description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;captured&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;url_regex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hasMatch&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;og_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;captured&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;image_regex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hasMatch&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;og_image&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;captured&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, we can enter URL-address and enjoy the preview and title&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fkhomin%2Fmeta-tag-parser-qml%2Fmaster%2Fpreviews%2Fpreview_4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fkhomin%2Fmeta-tag-parser-qml%2Fmaster%2Fpreviews%2Fpreview_4.png" title="Image" alt="alt text"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/khomin/meta-tag-parser-qml" rel="noopener noreferrer"&gt;here is the complete example (github)&lt;/a&gt;&lt;/p&gt;

</description>
      <category>qml</category>
      <category>cpp</category>
      <category>tags</category>
      <category>qt</category>
    </item>
  </channel>
</rss>
