diff options
-rw-r--r-- | content/clopher/03-TUI.md | 207 | ||||
-rw-r--r-- | content/clopher/04-Native_interface.md | 273 | ||||
-rw-r--r-- | content/hiding_the_complexity.md | 133 | ||||
-rw-r--r-- | content/screencast.md | 109 |
4 files changed, 722 insertions, 0 deletions
diff --git a/content/clopher/03-TUI.md b/content/clopher/03-TUI.md new file mode 100644 index 0000000..fb5ad02 --- /dev/null +++ b/content/clopher/03-TUI.md @@ -0,0 +1,207 @@ +Title: TUI: A look to the deep +Date: 2019-05-30 +Categories: Series +Tags: Clopher - TUI Gopher client +Slug: clopher03 +Lang: en +Summary: Research and work on the Terminal based User Interface of Clopher, the + gopher client. + +> Read all the posts in [Clopher series here]({tag}clopher-tui-gopher-client). + +This software have been introduced as a Gopher client but, as you can probably +deduce from the previous post, the Gopher part is probably the simplest one. +The complexity comes with user interaction. *People are hard*. That's why we +are going to delay that as much as possible, trying to cover all the points in +the middle before we *jump to the unknown*. + +Just joking. In fact, we have to shave many yaks before thinking about user +interaction anyway. This text talks about them. + +### Are you talking to me? + +Let's remember we can classify programs by two different categories like this: + +- **Non-interactive programs**, often called *scripts*, are programs that take + an input and return an output. There's no interaction with the user in + between. An example of this could be the command `ls`. + +- **Interactive programs** receive user input while they run and respond to the + user while they are running. An example of this could be the machine that + sells you the tickets for the subway, it asks you where are you going, then + tells you the price, take your money and so on. All of this with the program + constantly running. + +Remembering what we talked about Gopher: it's a *stateless* protocol. There's +no *state* stored in the server so all the queries *must* contain all the info +related to them. *Queries are independent*. + +This, somehow leaves the door open to two possible implementations of Clopher. +The *non-interactive* one would work like `curl`. Getting the IP, port, +selector string and an optional search string as input it would open the +connection retrieve the result and return it.[^1] + +But Clopher is designed as an **interactive program**. More like `lynx`, where +you interactively ask for the pages and have a *local state* that records your +history and other things. This is a decision, it's not imposed by the protocol. + +### Shell*f boycott* + +There are some different ways to handle user interaction in TUI based programs. +The simplest one is to read by line, waiting until the user hits `ENTER` to +read the result. That's the behaviour of the classic `scanf` function of C and +many others like `input` in Python, etc. + +In programs like Clopher, where the design is similar to `lynx` or `vi`, this +kind of input makes no sense at all. The program needs to be able to capture +every key pressed by the user and perform action in response to them. For +instance, in `vi` when the user hits `i` in normal mode it needs to change to +insert mode and when the user presses `i` in insert mode it needs to change the +contents of the buffer. + +The design of these kind of programs is simple to understand, it's an infinite +loop[^2] where key presses are captured and they change the *state* of the +program. When the user hits the key combination that halts the program the +loop is broken. + +In simple C code the program would look like this: + + ::clike + #include<stdio.h> + + int + main(int argc, char * argv[]){ + char c; + // Create some state + + while(1){ + c = getchar(); + if( c == 'q'){ // Exit if user pressed `q` + return 0; + } + // Update state here + putchar(c); // Show the character for debugging + } + } + +Or the simplified Clojure equivalent: + + ::clojure + (loop [c (char (.read *in*)) + state (->state)] ; Create some state + + (when (not= c \q) ; Exit if user pressed `q` + (print c) ; Show the character for debugging + (recur (char (.read *in*)) + (update-state c state))) ; Update state + + +Looks simple, right? + +Wait a second, there's a lot of stuff going on under the hood here. If you run +the code in any POSIX compatible operating system (I didn't test on others, +and I won't) you'll find the code might not be doing what we expected it to: +The `getchar` (or `.read`) calls will wait until `ENTER` is pressed in the +input buffer and then they'll get the characters one by one. But we want to get +them as they come! + +#### Saints and demons — canonical mode + +In POSIX operating systems, the input is buffered by default. But that behavior +can be configured following the POSIX terminal interface under the name +**canonical** mode or **non-canonical** mode. The mode we are looking for is +the non canonical mode. You can read more about it in [the Wikipedia][canon]. + +Choosing the non-canonical mode has some extra options: one controls the number +of minimum characters to have in the buffer to perform a `read` operation and +the other defines the amount of tenths of second to wait for that input[^3]. +Choosing the right value for those fields (`c_cc[MIN]` and `c_cc[TIME]`) +depends on the kind of interaction we are looking for. + +#### Make Dikembe smile — blocking + +Setting `c_cc[TIME]` field to `0` means the `read` operation will wait +indefinitely until the minimum amount of characters defined with `c_cc[MIN]` +are waiting in the buffer. Together with that, the `c_cc[MIN]` can be `0` that +means the read operations will wait until there are `0` characters in the +buffer, or, in other words, they won't wait. + +Be aware that both fields can provoke the read operations in the input buffer +be non-blocking operations and that will cause the read operation to return +with no value. + +In the case of Clopher, I decided to set the `c_cc[MIN]` to `1` so the read +operations block until there's at least one character in the buffer (that means +they will always return something) and the `c_cc[TIME]` to `0` so the read +operations have no timeout and will block until a character arrives. + +Depending on the application you are developing, you might choose other kind of +blocking configuration. For instance, setting a timeout can let you process +other parts of the system and wait for the input in the same thread. + +#### We're talking about practice? — termios + +So now we know where to find this theoretical configuration it's time to put it +in practice. In POSIX the standard way to access this is via `termios`[^4]. +It has some details that are not specified and depend on the implementation, so +it might have some differences from Linux to BSD or whatever. + +`tcsetattr` and `tcgetattr` calls can be used to set and read the terminal +configuration via termios. Check this example, compile it and compare it with +the C code of the previous example: + + ::clike + #include<stdio.h> + #include<termios.h> + + int + main(int argc, char* argv[]){ + // Get interface configuration to reset it later + struct termios term_old; + tcgetattr(0, &term_old); + + // Get interface configuration to edit + struct termios term; + tcgetattr(0, &term); + + // Set the new configuration + term.c_lflag &= ~(ECHO | ECHONL | ICANON | IEXTEN | ISIG); + term.c_cc[VMIN] = 1; // Wait until 1 character is in buffer + term.c_cc[VTIME] = 0; // Wait indifinitely + //TCSANOW makes the change occur immediately + tcsetattr(0, TCSANOW, &term); + + char ch; + while(1){ + if(ch == 'q'){ + // Set old configuration again and exit. + // If it's not set back the normal configuration of the + // terminal will be broken later! + tcsetattr(0, TCSANOW, &term_old); + return 0; + } + ch = getchar(); + putchar(ch); + } + } + +All the code has enough comments to be understood but there are some weird +flags it's better to check in termios documentation.[^4] + +### But this is C code and Clopher is written in Clojure! + +I know but this is becoming long and boring. Why not wait until I get some +spare time and write the next chapter? You have tons of information to check +until I write it so you won't be bored if you don't want to. + +See you next. + + + +[^1]: In fact, you can navigate the Gopherverse like this with `curl`. +[^2]: Unsurprisingly called *main loop*. Programmers are very creative. +[^3]: That `read` operation is what `getchar` is doing under the hood. +[^4]: `man termios` or visit [online `man` + pages](https://linux.die.net/man/3/termios) + +[canon]: https://en.wikipedia.org/wiki/POSIX_terminal_interface#Input_processing diff --git a/content/clopher/04-Native_interface.md b/content/clopher/04-Native_interface.md new file mode 100644 index 0000000..08a8e52 --- /dev/null +++ b/content/clopher/04-Native_interface.md @@ -0,0 +1,273 @@ +Title: TUI Slang: Speak like the natives +Date: 2019-06-15 +Categories: Series +Tags: Clopher - TUI Gopher client +Slug: clopher04 +Lang: en +Summary: Interfacing between Clojure, Java and Native code. + + +> Read all the posts in [Clopher series here]({tag}clopher-tui-gopher-client). + +The previous post introduced `termios` as a native interface to configure the +terminal input processing. With termios we managed to make our C programs get +input character by character processing them as they came with no buffering but +we didn't integrate that with our Clojure code. Now it's time to make it. + + +### Run before it's too late + +Before we dig in the unknown, I have to tell you there are other alternatives +for the terminal configuration. The simplest one I can imagine is using +`stty`[^1] as an external command. I learned this from [Liquid][liquid], a +really interesting project I had as a reference. If you want to see this work +check the `adapters/tty.clj` file in the `src` directory of the project.[^2] + +Of course, it has some drawbacks. `stty` is part of the GNU-Coreutils project +and you have to be sure your target has it installed if you want to rely on +that. I'm not sure about if it's supported in non-GNU operating systems[^3]. + +In my case, I decided to stay with `termios` interface to deal with all this +because I didn't really want to rely on external commands and it's supposed to +be implemented in any POSIX OS. The good (bad?) thing is it made me deal with +native libraries from Clojure and had to learn how to do it. + + +### The floor is Java + +When dealing with low-level stuff we have to remember Clojure is just Java, and +most of the utilities we need to use come from it. This means the question we +have to answer is not really *"how to call native code from Clojure?"* because +if we are able to call native code from Java, we will be able to do it from +Clojure too (if we spread some magic on top). + +#### So, how to call native code from Java? + +First I checked the [Java Native Interface (aka JNI)][jni], but I thought it +was too much for me and I decided to check further. Remember there are only a +couple of calls to make to `termios` from our code, so we don't really want to +mess with a lot of boilerplate code, compilations and so on. + +My research made me find [Java Native Access (aka JNA)][jna] library. If you +check the link there you'll find that the Wikipedia[^4] describes it as: + +> JNA's design aims to provide native access in a natural way with a minimum of +> effort. No boilerplate or generated glue code is required. + +Sounds like right for me. Doesn't it? + +I encourage you to check the full Wikipedia entry and, if you have some free +time at the office or something, to check the implementation because it's +really interesting. But I'll leave that for you. + +##### A lantern in the dark + +JNA is quite easy to use for the case of Clopher, even easier if you realize +there is [lanterna][lanterna], the TUI library, out there, using it internally +so you can *steal*[^5] the implementation from it. Lanterna is a great piece +of software I took as a reference for many parts of the project. Digging in the +internals of large libraries is a great exercise and you can learn a lot from +it. + +First of all, like many Java projects, the amount of abstractions it has is +crazy. It takes some time to find the actual implementation of what we want. +This isn't like this for no reason, the reality is they need to create this +amount of abstractions because the part of the library that handles the widgets +can work on top of many different terminal implementations, including a +[Swing][swing] based one that comes with Lanterna itself. + +Clopher only targets POSIX compatible operating systems so we can go directly +to what we want and read the termios part directly discarding all the other +compatibility code. This code is quite easy to find if you see the directory +tree of Lanterna: there's a `native-integration` folder in the root directory. +If you follow that you'll arrive to [`PosixLibC.java`][posix-lanterna] that +uses JNA to interact with termios. + +The implementation provided by Lanterna is quite complete, they declare a +library with the functions they need and the data structure introduced in the +previous chapter. Once the library interface and the necessary data structures +are defined from Java they can be called with JNA, like they do in the file: +[`NativeGNULinuxTerminal.java`][nativegnu-lanterna]. + +#### How to call JNA from Clojure, then? + +Calling Java code from Clojure is quite simple because Clojure have been +designed with that in mind, but this is not only that. Thanks to the Internet, +there's a great [blogpost by Nurullah Akkaya][nakkaya] describing a simple way +to use JNA from Clojure. From that, we can move to our specific case. + +`termios` has its own data structure so we need to define it so the JNA knows +how to interact with it. The problem is that Clojure doesn't have enough OOP +tools to do it directly so we need to make it in plain Java. The good thing is +that we don't really need to create anything else. + +If we remove some unneeded code from Lanterna's termios structure +implementation it will look like the implementation I made at +`src/java/clopher/Termios.java`: + + ::clike + package clopher.termios; + import com.sun.jna.Structure; + + import java.util.Arrays; + import java.util.List; + + + /** + * Interface to Posix libc + */ + public class Termios extends Structure { + private int NCCS = 32; + + public int c_iflag; // input mode flags + public int c_oflag; // output mode flags + public int c_cflag; // control mode flags + public int c_lflag; // local mode flags + public byte c_line; // line discipline + public byte c_cc[]; // control characters + public int c_ispeed; // input speed + public int c_ospeed; // output speed + + public Termios() { + c_cc = new byte[NCCS]; + } + + // This function is important for JNA, because it needs to know the + // order of the fields of the struct in order to make a correct Java + // class to C struct translation + protected List<String> getFieldOrder() { + return Arrays.asList( + "c_iflag", + "c_oflag", + "c_cflag", + "c_lflag", + "c_line", + "c_cc", + "c_ispeed", + "c_ospeed" + ); + } + } + +Once the struct is defined, it's time to use it from Clojure. `clopher.term` +namespace has the code to solve this. Summarized here: + + ::clojure + (ns clopher.term + (:import [clopher.termios Termios] + [com.sun.jna Function])) + + (def ^:private ICANON 02) + (def ^:private ECHO 010) + (def ^:private ISIG 01) + (def ^:private ECHONL 0100) + (def ^:private IEXTEN 0100000) + + (def ^:private VTIME 5) + (def ^:private VMIN 6) + + ; The macro we saw at the blogpost by Nurulla Akkaya + (defmacro jna-call [lib func ret & args] + `(let [library# (name ~lib) + function# (Function/getFunction library# ~func)] + (.invoke function# ~ret (to-array [~@args])))) + + ; Wrapper for the tcgetattr function + (defn get-config! + [] + (let [term-conf (Termios.)] + (if (= 0 (jna-call :c "tcgetattr" Integer 0 term-conf)) + term-conf + (throw (UnsupportedOperationException. + "Impossible to get terminal configuration"))))) + + ; Wrapper for the tcsetattr function + (defn set-config! + [term-conf] + (when (not= 0 (jna-call :c "tcsetattr" Integer 0 0 term-conf)) + (throw (UnsupportedOperationException. + "Impossible to set terminal configuration")))) + + ; Example to set the non-canonical mode using the flags at the top of the + ; file + ; Yeah, binary operations. + (defn set-non-canonical! + ([] + (set-non-canonical! true)) + ([blocking] + (let [term-conf (get-config!)] + (set! (.-c_lflag term-conf) + (bit-and (.-c_lflag term-conf) + (bit-not (bit-or ICANON ECHO ISIG ECHONL IEXTEN)))) + (aset-byte (.-c_cc term-conf) VMIN (if blocking 1 0)) + (aset-byte (.-c_cc term-conf) VTIME 0) + (set-config! term-conf)))) + + +Pay attention to all the mutable code here! +`aset-byte` function helps a lot when dealing with all that. + +Be also sure to check termios' documentation because the calls act in a very +C-like way, returning a non-zero answer when they fail. + +We need an extra point in our code to solve the Java-Clojure interoperability: +we have to tell our project manager that we included some Java code in there. +If our project manager is Leiningen, we can just tell it where do we store our +Java code. Be careful because Leiningen [doesn't like if you mix Java and +Clojure in the same folder][leiningen]. + + ::clojure + (defproject + ; There's more blablabla in here but these are the keys I want you to + ; take in account + :source-paths ["src/clojure"] + :java-source-paths ["src/java"] + :javac-options ["-Xlint:unchecked"]) + + +### Look back! + +Now you can configure your terminal to act non-canonically and serve you the +characters one by one as they come. It's cool but you'll see there are some +problems to come for the next chapters. Don't worry! They'll come. + +> This is like a heroic novel where the character (in this case you) fights +> monsters one by one leaving their dead corpses in the dungeon floor. Looking +> back will let you remember how many monsters did you slaughter in your way to +> the deep where the treasure awaits. Remember to take rest and sharpen your +> sword. This is a long travel. + +> Prepare yourself for the next monster. Let the voice of the narrator guide you +> to the unknown. + +Why don't you mix what you learned on the previous chapter with what you +learned from this one and try to make an interactive terminal program yourself? + +I'll solve that in the next chapter, but there's some code of that part already +implemented in the repository. You can check it while I keep writing and +coding. Here's the link to the project: + +[https://gitlab.com/ekaitz-zarraga/clopher][clopher] + + +See you in the next episode! + + +[^1]: Use the man pages, seriously: [`man stty`](https://linux.die.net/man/1/stty) +[^2]: I've also been in contact with Mogens, the author of the project, who is + a really good guy and gave me a lot of good information. +[^3]: But who cares about them anyway? +[^4]: *the free encyclopedia* +[^5]: If it's free software it's not stealing and it's exactly what you are + supposed to do with it. + +[liquid]: http://salza.dk/ +[jni]: https://en.wikipedia.org/wiki/Java_Native_Interface +[jna]: https://en.wikipedia.org/wiki/Java_Native_Access +[lanterna]: https://github.com/mabe02/lanterna +[swing]: https://en.wikipedia.org/wiki/Swing_(Java) +[posix-lanterna]: https://github.com/mabe02/lanterna/blob/master/native-integration/src/main/java/com/googlecode/lanterna/terminal/PosixLibC.java +[nativegnu-lanterna]: https://github.com/mabe02/lanterna/blob/263f013a2ee1d522eb86b8f1d315423fb1f79711/native-integration/src/main/java/com/googlecode/lanterna/terminal/NativeGNULinuxTerminal.java#L123 +[nakkaya]: https://nakkaya.com/2009/11/16/java-native-access-from-clojure/ +[leiningen]: https://github.com/technomancy/leiningen/blob/master/sample.project.clj#L302 +[clopher]: https://gitlab.com/ekaitz-zarraga/clopher diff --git a/content/hiding_the_complexity.md b/content/hiding_the_complexity.md new file mode 100644 index 0000000..7dcbb43 --- /dev/null +++ b/content/hiding_the_complexity.md @@ -0,0 +1,133 @@ +Title: Hiding the complexity +Date: 2019-10-27 +Category: +Tags: +Slug: hiding-the-complexity +Lang: en +Summary: + Inspired by scheme share my thoughts about the complexity and how it's + being hidden by our tools and how does that affect us as creators and + engineers. + +I've been recently playing with Scheme, reading R⁷RS and so on and I found +something really interesting: Even with its high level of abstraction, it +doesn't hide the complexity and makes you pay attention to it. + +That's extremely powerful and interesting. + +It's even more interesting if you think about the fact that Scheme can be +implemented from scratch in an acceptable amount of time by a couple of hands. +You don't need to be a big corporation or a big group of developers coding for +years to implement it. + +It's **simple** but it doesn't hide the complexity of the implementation. +That's a really powerful balance. + +But both points are too much to leave them here without playing with them +separately so let's try to understand why both of the points are[^1] +fundamental. + +[^1]: In my opinion, of course. This is *my* blog. + + +## <del>Hidden</del> Latent complexity + +You can create the best programming language in the world but the +complexity of the programming can't be eliminated because the user of the +language will be, actually, programming. So, you can take two approaches here: + +- Expose the **intrinsic** complexity of programming. +- Make it look as complexity doesn't exist. Which means hiding the complexity + as much as you can under layers of abstraction. + +Most of modern programming languages go for the second option. But not only +programming languages, also operating systems, computers themselves and many +areas more. Which is not specially bad in general, but it's dangerous when you +need control. + +Most of the times where complexity is hidden by design, it's just *latent +complexity*. It's harder to reach by the user, so the user scope of things they +can do is reduced (and with it the their ability to decide with a high level +of detail) but the complexity is still there, happening without being noticed, +under the surface and being impossible to correct if something goes wrong. + +Think about your cellphone. You can't open it, change the battery, change it's +software, change... *anything*. Because it's hard to do and "*people don't need +to know about that*". But finally, what you have is a phone that is impossible +to repair if something goes wrong or impossible to change if you want it to do +something that is not the default behaviour. + +It is a problem (some people is fixing trying to fix, by the way) but it's not +a problem for *everyone* because *everyone* doesn't need to have that level of +complexity exposed. But they should have the right to see it if they wanted to. + +## Exposed complexity + +As engineers working on technology, we should be demanding for the complexity +of things being exposed, more than running away from it. + +As engineers we are supposed to want to know how stuff works! + +In the case of programming languages, I want to control what the program does +and I be aware of what I'm doing and which decisions I'm taking. + +When complexity is exposed you are reminded of the importance of every choice +you make. It's not something that happens: you have to think about it. + +> In Scheme: List vs Vectors. Which one is better? Why have both? Why not use +> use one all the time? + +It's reminding you what do you have under the hood, even if you aren't +implementing it yourself. That way you don't forget about *your job*. + +## Simplicity + +Simplicity means that there's no unneeded complexity. It doesn't mean that +complexity is hidden. We tend to confuse both terms too often. + +Scheme is simple, because its core is small and it's based in few concepts. +Being simple means that concepts are clear and consistent and have few or none +exceptions. + +Other programming languages use the same concepts that scheme does but they are +not clearly stated so you can't rely on them for your understanding of a +language. Scope in JavaScript (for instance) is often explained as a thing +related with curly brackets' position while hiding the fact that it's a lexical +scope. Watching engineers prefer a silly trick than an academical fact is +unsatisfying[^2]. + +[^2]: And insulting, I'd say. + +In many platforms abstraction layers are added until the internals are hidden +or blurred. In this case, complexity is directly hidden by implementation, more +than by users themselves running away from it. + +Some would ask: "Who cares about the details?" [^3] And it's perfectly fine to +think that at some point but when it comes to choosing the right tool to the +right problem, performance and fine tunning, you'd really like to know how they +are implemented because implementation is what makes some operations be faster +or more accurate than others. And, probably more important than that, being +aware about how stuff is implemented make us independent enough to change the +implementation if we want, **which is the base of free software**. + +[^3]: TLDR: *You, as an engineer, should*. + +When your tools hide reality from you for long enough, you start to forget that +the reality still exists even if you are not watching it and you start +acting like it wasn't there. + + +## Assembly then... Right? + +Don't get me wrong. I'm not against simplification or making our job easier. +Scheme, is a really high-level language. **Abstraction is good**. + +Accidental self-lobotomy is not that good. + + +> NOTE: +> This blogpost was triggered by this talk where a musician talks about +> chiptune music and says how making chiptune music made him a better +> guitarist. It has some good points about constraints and complexity. +> +> <https://youtu.be/_7k25pwNbj8> diff --git a/content/screencast.md b/content/screencast.md new file mode 100644 index 0000000..5ab88c8 --- /dev/null +++ b/content/screencast.md @@ -0,0 +1,109 @@ +Title: Screencasts: discussing with ffmpeg +Date: 2020-01-11 +Category: +Tags: +Slug: ffmpeg-screencast +Lang: en +Summary: Screencasts in `ffmpeg`, having some fun solving issues thanks to + other people + +When you battle using your arguments that's called a *discussion*... That's +exactly what I've been doing for a couple of days with `ffmpeg`: I've been +using arguments trying to reach an understanding. + +I wanted to record my screen, and a couple of cameras, for reasons you'll know +about later, and I didn't want to play around with new GUI programs and +configuration so I decided to go for a simple `ffmpeg` setup. + +You probably had played with `ffmpeg` in the past. It has tons of different +input arguments and options. It's crazy. + +Most of my previous times using it were just video conversions and it's as +simple as choosing the right extensions for files, but when it comes to video +and audio recording it gets complicated. I have no idea about video and audio +encodings and I don't really have the time to dig in such an exciting topic. +I searched on the Internet for examples and I found some: cool. + +I played around with multiple inputs and outputs, I changed arguments I can't +even remember now and it kinda worked until I decided to record my voice at the +same time. **Delay**. + +What to do then? + +Just go to the internet an keep searching. + +I found a project called `vokoscreen` that now is archived because it's +migrating from `ffmpeg` to `gstreamer` (I also struggled with gstreamer in the +past but that's another story) but it worked fine. It was in the repos of my +distro, it only asked me to install one dependency, a couple of megs only... +Great! + +I tried to make a screencast and the audio worked like a charm. I went for the +code, read it and [realized the arguments it uses to call `ffmpeg` are easy to +find][vokoscreen]. + +Even better, in the program itself there's a button to show a log of what it +does and it dumps the exact call it does. + +With that and some extra things I learned from the investigation in the deep +abyss of the Internet, boom! There it goes: + + ::bash + ffmpeg + -y -f x11grab -draw_mouse 1 -framerate 25 -video_size 1920x1080 -i :0+0,0 \ + -f alsa -ac 2 -i default \ + -pix_fmt yuv420p -c:v libx264 -preset veryfast \ + -c:a libmp3lame -q:v 1 -s 1920x1080 -f matroska \ + output.mkv + +Today in half an hour I solved the thing I've been struggling with a couple of +days. But I think I wouldn't be able to solve it if I didn't struggle with it +last days... I don't know. + +The good thing is I learned a couple of things from this I'll write down here +to avoid forgetting them: + +### Multiple inputs + +Like the command in the example, `ffmpeg` can get multiple inputs. In the case +of the example, they are `x11grab` (my screen) and `alsa`'s default input (the +microphone). More inputs can be combined, like music playing in the background +or whatever you want. + +### Multiple outputs + +There's also the chance to put multiple outputs there just like the multiple +input thing does but in the output part of the command[^1]. + +#### Pipe + +You can even pipe the command to a different one, like: + +``` +ffmpeg [...] - | ffplay -i - +``` + +In this case you can use one of the outputs to record to a file and another +one to `ffplay` which plays the video in screen. + +This is useful if you want to record from a webcam and you want to see what you +are recording. + + +### Closing note + +So yeah, I was an ignorant about `ffmpeg` and I still am. + +But at least I learned a couple of the arguments and learned how to deal with +all my cameras and screens at the same time. + +Good enough. + +I mean, it works, right? And that's the most important thing[^2]. + +[^1]: Yes, it's hard to know where's the input and where's the output. +[^2]: It's not. The most important thing is to be happy, do what you like, + enjoy your life and feel appreciated and valued. If your software works it's + like... Uugh... Congratulations I guess? + +[vokoscreen]: https://github.com/vkohaupt/vokoscreen/blob/b5865a85561baa46e627c09cf77efb7369516327/screencast.cpp#L2718 |