jQuery-SVG UI

Recently I played a little with SVG, using the JavaScript library Raphael and later jQuery-SVG. I want to share some of my experiences with it. My test setup of browsers include:

Chrome 20.0.1132.57
Firefox 12.0
Opera 11.60

The idea I had, involved creating a user interface controlled by JavaScript. For the sake of this article I limit this to the following: Multiple windows constructed out of a border and background, a title bar and a ‘knob’ for resizing, that only appears on hovering. The user should be able to move the window by dragging the title bar and to resize the window by dragging the resize knob, both events should bring the window to the front.

Because I’d never done anything before with SVG, I used Raphael at first, a very easy to use API. In Raphael, I created a Set with the various elements: paths for the outline, title and resize knob. To move and resize the window I used the drag event provided by Raphael.

The problem with this, that I only later realized, is that Sets are just arrays to logically group elements together, they are not grouped within the SVG document. If you change a transformation on a Set, Raphael is manually updating all transformations on all elements within that Set, a potentially very expensive operation. This is how our window document would look like with Raphael:


Turns out SVG already supports real element grouping (d’oh) with the <G> element! That was the moment I ditched Raphael (even though there is an experimental plugin that uses <G> for sets, but who knows what else is done this inefficiently?). jQuery-SVG is a nice alternative that provides a more direct API for SVG manipulation.

The same document using this approach would look something like this:


Another interesting SVG feature I explored was the <DEFS> and <USE> elements. Its possible to declare groups within <DEFS> and to instantiate it with a <USE> later on, <USE> provides X and Y attributes so you don’t even need to transform/translate anything! That sounded really great in theory, but then the browser bugs started coming in:
Firefox has an 8 year old bug that prevents any events of elements within <DEFS> to be propagated (and a related bug: .getBBox() is also broken). Chrome and Opera work great until you discover their inability to show :hover effects correctly. At the end and after a lot of wasted time on (not so) possible workarounds, I decided to forget about <DEFS> and <USE> completely.

To theme the window you can use a stylesheet like this:

.outline, .title {
    fill: white;
    stroke: black;
.title:hover {
    fill: black;
.resize {
    fill: none;
    pointer-events: all; /* otherwise no mousemove event would be propagated */
.resize:hover {
    fill: black;

Now to create the window, I first created functions that return the paths based on the current dimensions of the window (you could also use translate in some cases, has that an impact on performance? Idk):

function outlinePath(width, height) {
    return [
        'm', 0.5, 0.5,
        'h', width,
        'v', height - 10,
        'l', -10, 10,
        'h', -width,
        'v', -(height-10),
        'l', 10, -10,
        'z'].join(' ');
function titlePath(width) {
    return [
        'm', 5.5, 5.5,
        'h', width-10,
        'v', 10,
        'l', -10, 10,
        'h', -(width-10),
        'v', -10,
        'l', 10, -10,
    ].join(' ');
function resizePath(width, height) {
    return [
        'm', width + 1.5, height - 10 + 1.5,
        'v', 10,
        'h', -10,
    ].join(' ');

and to create the svg structure procedurally:

var x = 50, y = 50, width = 100, height = 100;
var g = svg.group({id: 'window_id'}); 
function moveWindow(x, y) {
    svg.change(g, {transform: 'translate('+x+','+y+')'});
var outline = svg.path(g, outlinePath(width, height), {class_:'outline'});
var title = svg.path(g, titlePath(width), {class_:'title'});
var resize = svg.path(g, resizePath(width, height), {class_:'resize'});
moveWindow(x, y);

first lets allow to move the window by dragging the title:

var lastDragX, lastDragY;
var mousemove = function (event) {
    moveWindow(event.clientX - lastDragX, event.clientY - lastDragY);
    lastDragX = event.clientX;
    lastDragY = event.clientY;
var mouseup = function (event) {
    $(window).unbind('mousemove', mousemove);
    $(window).unbind('mouseup', mouseup);
$(title).mousedown(function (event) {
    lastDragX = event.clientX;
    lastDragY = event.clientY;
    $(window).bind('mousemove', mousemove);
    $(window).bind('mouseup', mouseup);

almost the same for resize, just instead of moving the window:

width += event.clientX - lastDragX;
height += event.clientY - lastDragY;
svg.change(outline, {d: outlinePath(width, height)});
svg.change(title, {d: titlePath(width)});
svg.change(resize, {d: resizePath(width, height)});

To bring a window to the front you can just do this:


You can look at a demo here.

This looks really easy now, but it took me a while to get to this point. I recommend anyone who wants to do something serious with SVG, to look at the specs and to use a low level API, also make sure to look at different browsers to see what bugs they throw at you.

Live Transcoding for Video and Audio Streaming

With slowly increasing upload bandwidths (even in Germany), video and audio streaming for home servers become feasible. In Germany, currently the highest upload bandwidth you can get as a non-corporate is 10 Mbit/s with VDSL, enough to provide actual viewable video streaming in near DVD quality to a small number of concurrent viewers. However it was kind of hard to get a setup working that suits my requirements.

In my case the starting point are video and audio files, the video files are mostly XviD (MPEG4) encoded (with one or multiple MP3 or AC3 audio streams) in AVI containers. The audio files are mostly MP3 files with a constant or variable bitrate.

Because I want to stream (more or less) directly to the browser, possible player technologies are: Browser Plugins (WMP/ActiveX, Mplayer/VLC Mozilla Plugins, Quicktime), Flash or HTML5 Video and Audio Tags.

All of them support different codecs, containers and streaming protocols. I use HTTP as a streaming protocol for a numerous of reasons, but lets just say the decisive factors are that HTTP is supported by all mentioned players and is easy to integrate in existing web applications.

Browser Plugins

The different player plugins are a mess, there is no standard what so ever and it is hard to make any cross platform assumption on support of this. It has the theoretical capability to stream my video and audio file formats but in my experience it is most likely to fail out of the box so I would not count on that.


The popular flash plugin supports the mediocre Sorenson Spark and VP6 video codecs and most recently H.264 video. The audio codecs are MP3 and most recently AAC. Flash uses its own Container Format FLV, in more recent versions other containers are also supported.

There are two notable non-free open source flash players. The jwplayer (custom commercial/open source license) with a phone home logo/branding and flowplayer with a (In my mind) violating GPLv3 dual license and branding.

Maybe I should mention that Flash supports their own proprietary streaming protocol RTMP, since its DRM infected (RTMPe) and just evil, I ignore it, even through there is an open source server implementation.

The major problem of flash video is (besides of its proprietary status) the exceedingly modest video decoder that requires high end hardware and obscene resources to play even medium quality video.


Most recent and upcoming browsers implement the Video and Audio functionality of HTML5. Unfortunately and due to the nature of HTML5, w3c does not specify video and audio codec requirements. As a result every browser and even different versions have a different subset of supported codecs. The formats are (Video Codec/Audio Codec/Container): Theora/Vorbis/OGG, H.264/AAC/MP4 and VP8/Vorbis/WebM. For more information visit the excellent site diveintohtml5.org.


Form to create streams with advanced settings.

I want support for HTML5 and Flash for my home server streaming setup, this should support streaming for all modern browsers and operating systems (even mobile devices). As you may have noticed, none of them can handle my commonly used file format and the variety of formats used for HTML5 makes transcoding unavoidable.

Transcoding means the encoding from one digital video/audio file to another. So the solution would be to transcode all files to the required formats (at least Sorenson Spark, H264 and OGV) beforehand. And now comes the tricky part: Because for some reasons (too many files, no disk space to waste) this is not an option in my case. Now normally that would be the end of this article, but I came up with a solution although it has its own problems and limitations.

Live Transcoding

The basic idea is to start the transcoding process the same time the user requests streaming for a audio/video file. It works kind of like a live feed, but instead of using the video and audio stream from a camera and microphone, it reads video and audio files. Because it starts a resource hungry encoding process for each stream, the maximum possible concurrent viewers are very limited. In my case, because my upload bandwidth is limited anyway, this is not really a problem, but in other, larger setups, it sure is.

Another big limitation is seeking, although theoretically possible with pseudo HTTP streaming, it is very difficult to implement in this kind of live streaming. I have some vague ideas how this could work, but I’ve not implemented any of it. As a kind of workaround, the user can specify a timecode, FFmpeg should seek to, before the transcoding starts.

One last thing: I talked about the different formats supported by HTML5 capable browsers. Some of them only support H.264/AAC within MP4 container. The problem is that, the MP4 container is not capable of streaming this way because it needs to seek inside the output file. FFmpeg will just quit with the message muxer does not support non seekable output. Theres a alternative for Apple stuff but I do not plan to implement that anytime soon.

Proof of Concept

I’ve created a proof of concept implementation using PHP, FFmpeg for transcoding and MPlayer to identify encodings and bitrates. The user can select player technology, audio/video bitrate (to transcode for lower bandwidths) and adjust the transcoding settings in detail. My solution is pointed to technical experienced users, depending on your audience you probably would use browser sniffing to hide technical details completely. The scripts need Linux, PHP5 and the Smarty Template Language installed. If you want to give it a try I’ve created a github repository: php-live-transcode. Make sure you met the requirements and change the config.inc.php file to match your setup.


I’m well aware about the different problems of this peace of software, although some could be fixed, there are some fundamental problems that are just unavoidable, so it is important that you are aware of the limitations before you use any of this. Notice the configuration value FFMPEG_MAX_INSTANCES, this defines how many ffmpeg instances should be allowed to run simultaneously, this is very important because this setup just calls out for a Denial of Service attack, so be careful.

I guess thats it for now, please leave a comment if you have tried it. Please use the issue tracker at github if you encounter a problem.

JavaScript: onMouseMove Google Translation

I wanted to implement a JavaScript onMouseMove Event, that translates the word under the mouse pointer with the Google Translator. I want to share the following solution.

I split this into the following two separate problems:

First: Detect word for an onMouseMove Event

The onMouseMove Event can listen on any Element for mouse movements. To detect the word under the mouse pointers position, I utilize the W3C(DOM-2) specified Range Object.

Most modern browsers support the Range object, but the event.rangeParent Attribute seems to be only supported by Firefox. Currently I’ve no cross-browser solution for this.

 * Apply offset to range and extract a single character at position.
 * range -- the range object effected
 * originalOffsetStart, originalOffsetEnd -- Start offsetStart and offsetEnd
 * offset -- the relative offset to apply
function getRangeCharacter(range, originalOffsetStart, originalOffsetEnd, offset) {
    var character = ' ';
    try {
        range.setStart(range.startContainer, originalOffsetStart + offset);
        range.setEnd(range.startContainer, originalOffsetEnd + offset);
        character = range.toString()[0];
        range.setStart(range.startContainer, originalOffsetStart - offset);
        range.setEnd(range.startContainer, originalOffsetEnd - offset);
    } catch (e) {}    

    return character;

 * Returns a single word for the given event.
function getEventWord(evt) {
    if (!evt.rangeParent || !document.createRange) {
        return '';

    var range = document.createRange();

    range.setStart(evt.rangeParent, evt.rangeOffset);
    range.setEnd(evt.rangeParent, evt.rangeOffset);

    // the word ends when this characters appears
    var stop_character_pattern = /^[\W]$/m;

    // this "overwrites" some characters from the above pattern
    var ignore_stop_character_pattern = /^['|\-]$/;

    // I assume startOffset == endOffset so set 1 character selection ...
    var originalOffsetStart = range.startOffset;
    var originalOffsetEnd = range.startOffset + 1;
    try {
        // test:
        range.setStart(range.startContainer, originalOffsetStart);
        range.setEnd(range.startContainer, originalOffsetEnd);
    } catch (e) {
        // out of bounds
        originalOffsetStart -= 1;
        originalOffsetEnd -= 1;
        range.setStart(range.startContainer, originalOffsetStart);
        range.setEnd(range.startContainer, originalOffsetEnd);

    // First Step: Find left end of word:
    var leftCharacterPos = 0;
    var iChar = '';
    for (var iOffStart = 0; iOffStart <= originalOffsetStart; iOffStart++) {
        iChar = getRangeCharacter(range, originalOffsetStart, originalOffsetEnd, -1 * iOffStart);

        leftCharacterPos = iOffStart;

        if (stop_character_pattern.test(iChar) && !ignore_stop_character_pattern.test(iChar)) {
            // remove the stop character!
            leftCharacterPos -= 1;
    } leftCharacterPos = originalOffsetStart - leftCharacterPos;

    // Last Step: Find right end of word:
    var rightCharacterPos = 0;
    for (iOffStart = 0; true; iOffStart++) {
        iChar = getRangeCharacter(range, originalOffsetStart, originalOffsetEnd, iOffStart);

        if (stop_character_pattern.test(iChar) && !ignore_stop_character_pattern.test(iChar)) {
            rightCharacterPos = iOffStart - 1;
    } rightCharacterPos = originalOffsetEnd + rightCharacterPos;

    try {
        range.setStart(range.startContainer, leftCharacterPos);
        range.setEnd(range.startContainer, rightCharacterPos);
    } catch (e1) {
        return '';

    var retWord = range.toString();


    return retWord;

Second: Translate Text with Google Translator

This is very easy because Google provides an very easy to use Ajax API for this (attend the possible google user tracking!):

// read onMoveTranslate()
var last_word = null;
var timeout = null;
var word_cache = [];

 * Translate text For onmousemove events.
function onMoveTranslate(event) {
    // get element the translation should appear in:
    var translation = document.getElementById('translation');

    var word = getEventWord(event);
    if (!word || word == '') {

    if (typeof(word_cache[word]) !== 'undefined') {
        translation.innerHTML = word_cache[word]; //+" (cached)";

    if (word != last_word) {
        if (timeout) {

        translation.innerHTML = 'Translating ... ' + word;
        timeout = window.setTimeout(function(){
            google.language.translate(word, 'en', 'de', function(result) {
                if (!result.error) {
                    translation.innerHTML = result.translation;
                    word_cache[word] = result.translation;
        }, 800);
    last_word = word;

google.load("language", "1");

At last I assign the onMouseMove Event to the onMoveTranslate() function:

Here is an full Example for this.

mtget: ZDF Mediathek Download/Stream

Nachdem die ZDF Mediathek nun ihr lang erwartetes relaunch ‘feiern’ kann, habe ich mich daran gemacht mtget zu überarbeiten. Die neue, für Linux Anwender besonders interessante, “HTML Version”, macht, da ohne Flash, die ZDF Mediathek nun erstmals halbwegs benutzbar. =)

Die asx-Links fürs mms streaming werden jetzt auch nicht mehr versteckt, so im großen und ganzen also ein ziemlicher Fortschritt. Für die Nerds unter euch die dennoch gern eine Shell-Lösung hätten, pflege ich mtget trotzdem weiter.
Bei der Gelegenheit habe ich mtget in Python komplett neu geschrieben. Version 0.5 kommt ohne zusätzliche Libraries aus, es müssen lediglich Python(bei mir läuft 2.6) sowie die in CMD_STREAM und CMD_DOWNLOAD eingetragenen Programme(per default: mplayer) installiert sein. In der neuen Version werden erstmals Kanäle erkannt und können im interaktiven Modus(-i) ausgewählt werden, andernfalls wird ihnen automatisch gefolgt und die Einträge abgespielt. Anders als die vorigen Versionen lädt 0.5 nur die mms-Streams. mtget.py:

mtget.py: Interaktive Auswahl der Videos und Kanäle

mtget.py: Interaktive Auswahl der Videos und Kanäle

ZDF Mediathek Download/Streaming Skript
v0.5 <apoc@sixserv.org> http://apoc.sixserv.org/
Stand: 2009-12-20

Syntax: ./mtget.py <URL/ID> [OPTIONS]
  <URL/ID>                   mediathek video/kanal url oder id  
  -1                         qualitaet DSL 1000
  -2                         qualitaet DSL 2000 (Standard)
  -m, --mode <d/s>           download(d) oder streaming(s)
  -d, --dir <directory>      das verzeichnis wohin gespeichert werden soll(.)
  -t, --title                benutzt nicht den stream dateinamen sondern titel
  -s, --search <topic>       suche in der mediathek
  -l, --maxr <max>           wieviele ergebnisse verarbeiten(suche/kategorie)
  -c, --ignore-channel       ignoriert kanaele
      --no-colors            deaktiviert die kursiv und fettschrift
  -i                         interaktiv, auswahl der zu spielenden videos
  -v                         erweiterte ausgabe, zu debugging zwecken
  -h, --help                 zeigt diese hilfe

Mir sind übrigens noch zwei weitere Projekte bekannt die ebenfalls Mediathek streaming bieten: mtscrape und zdfmediathk.

Für Vorschläge, Bugreports und alles andere hinterlasst mir ein Kommentar oder schreibt eine eMail an apoc@sixserv.org!

Content Extraction Algorithmen: Density/CCB

Im Artikel, “Web scraping mit Ruby/Mechanize” zeigte ich wie man Webseiten von einem Ruby Skript laden lässt und definierte Inhalte daraus extrahieren kann. Diese Verfahren, z.B.: Regular expression, XPath oder CSS Selektoren, funktionieren nur für Seiten deren Struktur während der Implementierung bekannt sind. Diesmal allerdings geht es um Algorithmen die den Hauptinhalt(z.B. den Text einer Nachricht) von jeder beliebigen Seite erkennen und extrahieren können. Diese Möglichkeit ist vor allem für Suchmaschinen/Datamining oder für Anwendungen die die Lesbarkeit erhöhen wollen, z.B. für kleine Bildschirme(Handhelds) oder Screen Reader, interessant.

Auch wenn es ein recht neues Forschungsfeld der Informatik ist, gibt es schon einige Veröffentlichungen zu dem Thema. Für den Einstieg am Interessantesten ist wohl die Dissertation von Thomas Gottron, “Content Extraction: Identifiying the Main Content in HTML Documents“, die einen Überblick über die Algorithmen gibt, eine Möglichkeit der Evaluation beschreibt und einige neue Algorithmen einführt. Das erfolgversprechendste dieser Verfahren, der CCB(Content Code Blurring) Algorithmus ist auch Thema dieses Artikels.

Einen weiteren interessanten Ansatz liefern Arias Moreno, Deschacht und Moens in Ihrem Paper “Language Independent Content Extraction from Web Pages” deren Verfahren(Density) hier auch besprochen wird.

Natürlich kann man von keinem der Algorithmen Perfektion erwarten, dafür sind die Webseiten zu unterschiedlich strukturiert. Auch will ich nicht vergessen zu erwähnen das diese Algorithmen nur auf Seiten mit klar erkennbarem Hauptinhalt(Main Content) funktionieren, Weblogs z.B. verwirren so gut wie alle Verfahren die darauf Basieren die höchste “Text Konzentration” auf einer Seite zu finden, da die Kommentare unter dem Artikel nicht selten wesentlich umfangreicher als der Hauptinhalt sind(z.B. Slashdot).

Content Code Blurring (Python Skript / Modul)

Der CCB Algorithmus von Thomas Gottron stellt ein Verfahren dar, um die Teile einer HTML-Seite zu identifizieren die aus viel Text und nur wenigen Tags besteht. Dazu wird die Seite in eine Sequenz aus Tokens oder Zeichen aufgeteilt: Die TCCB Variante teilt die Sequenz in Tokens aus Tags oder Wörtern ein, sonst werden Zeichen unterschieden die sich innerhalb(Code) oder außerhalb(Inhalt/Content) eines Tags befinden. Der so entstandene Content Code Vektor(CCV) speichert für jeden Code ein 0 und für jeden Inhalt eine 1.

Jetzt wird der CCV verwischt(Blurring) dazu wird ein Gaussian Blur Filter verwendet, der im Prinzip einer Gaussian Distribution/Normal Distribution entspricht. Anders gesagt werden die Inhalts(1)-Elemente des CCV auf ihre Nachbar-Elemente verstreut oder verwischt. Dieser Vorgang wird n-mal wiederholt oder bis sich der Prozess normalisiert hat(wobei ich es leider nicht geschafft habe das zu implementieren(kA. woran das liegt, GERNE FEEDBACK!)). Meine Beispielimplementierung kann ein Diagramm des CCV “plotten”:

Dieser Plot zeigt, dass man schon auf der richtigen Spur ist. Klar erkennbare Spitze wo der Hauptinhalt liegt.

Dieser Plot zeigt, dass man schon auf der richtigen Spur ist. Klar erkennbare Spitze wo der Hauptinhalt liegt.

Am Ende werden die Teile des CCV welche über einen bestimmten Schwellwert liegen mit ziemlicher Sicherheit zum Hauptinhalt der Seite gehören. (Im Diagramm sehr leicht zu erkennen.)

Der CCB Algorithmus funktioniert sehr gut und gilt als einer der besten Algorithmen für Content Extraction überhaupt. Ein Nachteil besteht darin das Teile der Seite die ebenfalls viel Text enthalten mitunter zum Hauptinhalt gezählt werden, auch wenn sich diese Bereiche weit entfernt vom eigentlichen Inhalt befinden.

Density (Ruby Beispiel)

Der Density Algorithmus von Arias Moreno, Deschacht und Moens folgt einem ähnlichen Ansatz, die HTML-Seite wird durch einen (möglichst robusten) XML-Parser geparsed. Dann wird jeder (XML-)Node des Dokuments der Text enthält an den letzten String eines Arrays angehängt. Handelt es sich bei dem Node um einen HTML-Tag der die Struktur des Dokuments verändert(z.B. p, table, br, div, h1-6, li usw.) wird das Array um einen leeren String erweitert.

Wurde dieses Array fertig erstellt, wird ein Algorithmus angewandt, der den Bereich mit der größten Textdichte im Array zu bestimmen versucht(siehe Paper). Am Ende wird das Array nur noch sortiert und das Ergebnis steht als Plaintext fest.

Density arbeitet in der Praxis sehr gut, jedoch wird das Ergebnis sehr stark von den beiden Parametern beeinflusst, die den Algorithmus zum erkennen des Bereichs einstellen(besonders wieviele Absätze am Stück erkannt werden).

Update: Ich habe eine Implementierung für Density in Ruby eingefügt, er versucht den Hauptinhalt dieses Artikels zu extrahieren :)

Web scraping mit Ruby/Mechanize

Praktisch jede Interaktion mit einer Website oder Webapplikation kann gescriptet, d.h. automatisiert werden. Das Abgrasen von Webseiten nach bestimmten Informationen wird auch als Scraping bezeichnet(für die nicht menschlichen Besucher dieser Seite sei das erwähnt *g*) Scripte können einem eine ganze Menge Arbeit abnehmen und sogar Dinge tun, die manuell unmöglich wären. Ich beschäftige mich mit dem Thema schon seit einer ganzen Weile und möchte hier nun die von mir favorisierte Methode dafür vorstellen, die Bibliothek Mechanize für die Scriptsprache Ruby.

Mechanize hat seinen Ursprung in Perl, mittlerweile gibt es jedoch auch Implementierungen der API für Python und eben Ruby. Für PHP gibt es mit Snoopy ein ähnliches Projekt, wenn es auch bei weitem nicht so fortgeschritten ist. Mechanize bietet die Möglichkeit mit einfachen Methoden eine art Webbrowser zu simulieren. Alle Beispiele wurden mit Mechanize Version 0.9.2 und Ruby 1.8.7 getestet.

Installation / Initialisierung

Mechanize kann mit Gems(ähnlich CPAN oder PEAR) installiert werden(# gem install mechanize --remote), einige Distributionen bieten aber auch eigene Pakete an. Ein Ruby-Script kann daraufhin Mechanize inkludieren und ein Objekt erstellen:

require 'rubygems' # ist unter Umständen notwendig
require 'mechanize'
agent = Mechanize.new

Jetzt ist Mechanize einsatzbereit, die folgenden Beispiele bauen darauf auf. Außerdem können mit dem agent nun noch grundlegende Einstellungen vorgenommen werden:

agent.set_proxy('localhost', '8000')
agent.user_agent = 'Individueller User-Agent'
agent.user_agent_alias = 'Linux Mozilla'

Die Einstellung ‘user_agent_alias‘ wählt einen User-Agent String aus dem folgenden Satz von Beispielen aus: Windows IE 6, Windows IE 7, Windows Mozilla, Mac Safari, Mac FireFox, Mac Mozilla, Linux Mozilla, Linux Konqueror, iPhone und Mechanize. Die Timing Einstellungen können ebenfalls sehr wichtig sein:

agent.open_timeout = 3 # setzt timeouts
agent.read_timeout = 4
agent.keep_alive = false # default ist true

Hier folgen nun einige Beispiele, vielleicht werde ich mit der Zeit auch noch ein paar ergänzen, falls jemand Vorschläge hat, immer her damit. Ich habe auf http://apoc.sixserv.org/requestinfo ein kleines Skript am laufen das nützliche Informationen zum HTTP-Request liefert, das kann zum Experimentieren mit Mechanize sehr nützlich sein. Einige Beispiele findet man auch in den GUIDE und EXAMPLES Dateien des Mechanize Pakets. Continue reading