i18n of database

We need to have way to work with i18n databases too.
Resource for some thoughts: https://github.com/TAPevents/tap-i18n-db

Seems that issue isn't that simple.
Sometimes we need to store something not just in templates or script, but in database. For example, products with their specs, user entries, etc.
Some of their properties might need l10n.
What's the problem, Doc?
We can't (or to be more correct — shouldn't) gettext database queries like that:
gettext(database.entry)

Reasons:

Extractors won't be able to get actual content of database.entry
Even if first issue will be beaten (and it's possible by rendering, let's say, Nunjucks to html and getting text from there), we can't locolize in such way whole database — if some property won't be called with gettext, it will be never localized
We can't serve gettexted translations via API, since database actually never mutaded with new translations. We can map this somehow... but it doesn't look like right way of doing things

So, what of that, pal?
So far the only reasonable way seems to hold l10n right inside database entries. It will look like this:
---
product:
  property1:
    _origin: text string # this string will be used for `baseLocale`
    _de-DE: Zeichenfolge
    _uk-UA: строка тексту
    _ru: строка текста # will match `ru-RU` and `ru-UA`
  subproperty:
    property2:
      _origin: text string 2
      _de-DE: Zeichenfolge 2
      _uk-UA: строка тексту 2
      _ru: строка текста 2
  plurals:
    apple:
      _origin: # array tells to function that it should be handled as plurals, like `ngettext`
        - %(count) apple
        - %(count) apples
      _de-DE:
        - %(count) Apfel
        - %(count) Äpfel
      _uk-UA:
        - %(count) яблуко
        - %(count) яблука
        - %(count) яблук
      _ru:
        - %(count) яблоко
        - %(count) яблока
        - %(count) яблок

To access such data we will need special functions:

Get data for current locale:
tap(data.product.property1) -> text string

Get data for specicif locale:
dtap('de-DE', data.product.property1) -> Zeichenfolge

Get plural data for specific locale:
dntap('de-DE', data.product.plurals.apple, 2, 2) -> 2 Äpfel

Pros:

Database can be actually localized
It's easy to get data from
It can handle plurals

Cons:

We can't send it to translation platforms in such forms, and there is no easy way to extract messages for translation — probably we'll need parser for it. Or handle it manually (which will be possible only with small database)
Even if we'll manage to extract messages for translation, we will have to find way to dump it back into database in appropriate form
To serve such database as API, it have to be mutated (API/get/product/en should return us only English strings, instead of all). But I think it isn't big issue. Example: http://www.drzon.net/approaches-to-json-internationalisation-i18n/
A lot of l10n-related properties cluttering database and makes it harder to read.
And worst from all to me — we're introducing whole new layers, making thinks inconsistent by using gettext for strings, and tap (or whatever name it will get) for accessing database quries, which require different handling, different way of extraction, etc.

Kind of bonus
Just to mention, tap-i18n-db uses similar approach, but a bit different structure:
---
product:
  property1: text string # this string will be used for `baseLocale`
  subproperty:
    property2: text string 2
  plurals:
    apple:
      - %(count) apple
      - %(count) apples
  i18n:
    property1:
      de-DE: Zeichenfolge
      uk-UA: строка тексту
      ru: строка текста
    subproperty:
      property2:
        de-DE: Zeichenfolge 2
        uk-UA: строка тексту 2
        ru: строка текста 2
    plurals:
      apple:
        de-DE:
          - %(count) Apfel
          - %(count) Äpfel
        uk-UA:
          - %(count) яблуко
          - %(count) яблука
          - %(count) яблук
        ru:
          - %(count) яблоко
          - %(count) яблока
          - %(count) яблок

But it works pretty much the same, as described above example.
Pros:

Much cleaner original object

Cons:

No way to mark which strings should be translatable (thus, which one should be extracted and sent to translation platform)
I guess that approach assumes, that whole database should be translatable.
However, entries, which have 100-200 properties, from which only 1-2 should be translatable, looks like problematic to me...

Duplication of structure, which makes harder to maintain YAML or JSON-based manually managed databases

Some insane idea struck my head. What if we could do it like this?
---
product:
  property1: {{ gettext('text string') }}
  subproperty:
    property2: {{ gettext('text string 2') }}
    partial: string where only {{ gettext('some part') }} translatable
  plurals:
    apple: {{ ngettext('%(count)s apple', '%(count)s apples', %(count)s, %(count)s) }}

It so crazy, that actually might work!
For getting data from such database we'll need special functions:

Get data for current locale:
dbgettext(data.product.property1) -> text string

Get data for specicif locale:
dbdgettext('de-DE', data.product.property1) -> Zeichenfolge

Get plural data for specific locale:
dbdngettext('de-DE', data.product.plurals.apple, 2, 2) -> 2 Äpfel

Those are, in fact, very similar to original gettext functions, but they will:

Forcefully render Nunjucks template inside
Pass into inner gettext function arguments (just remapping, nothing more)
By rendering Nunjucks template execute inner gettext function
Return whole property with backed-in translation

Props:

Clean original database, without tons of nested properties in forms of locales

Single i18n way for database and for templates

gettext inside database in theory can be extracted by any parser (for example, in JavaScript mode)

Translators will receive strings from such database in form of ordinary .po file, so they even won't realize that they're working with database translation

Extractors will merge all duplicates of strings, unless context will be provided

Cons:

So far I don't see possibility to use more than one gettext in single property, otherwise it won't be possible to decide to which inner gettext should be mapped dbgettext arguments. Though, probably it's solvable

A bit unordinary database

No way of retrieving data from such database without special functions

For external API it should be somehow pre-rendered, which is kind of not what you'd expect from yaml and json files

And how are you planning to do selects/filter/map from this kind of data? :)

More specific example - search

Pre-render it? :) So far I don't know, it's just an idea.

Okey, some thoughts about "crazy" approach.
First of all, I've tried to use {{ gettext('String') }} right inside yaml files and then render those data-files into temporary folder, from which Nunjucks and other modules can grab it.
Some significant cons:

You're stuck with data-files only — you can't render in such way, for example. data, specified right inside Gruntfile. However, it's solvable by importing gettext functions inside Gruntfile
You have to generate new data file for each locale
On change of any data file, you will kick-off Grunt's regeneration of data files and templates... it results in quite a lot of generations, and they aren't quick
You have to put extra tasks inside grunt and grunt:build to generate data actually before it will be passed anywhere, since you're requiring it inside Gruntfile... it's end up quite unclear what happening

Pros:

You receive prepared and localized data, nothing extra should be done to work with it

Then I've tried another, "live" method.
Instead of pre-generating data with Nunjucks via Grunt, I've tried to do it in "live" mode. We'll just use function, which takes passed in query and renders it with Nunjucks:
###*
 * Render `gettext` function inside of database query result
 * @param {object} query                  Object to render and invoke it's `gettext` functions
 * @param {string} locale = currentLocale Locale, from which string should be loaded
 * @return {string|object} Localized result
###
env.addGlobal 'dbgettext', (query, locale = currentLocale) ->
  if locale != currentLocale
    @.ctx.page.locale    = locale
    @.ctx.page.isoLocale = isoLocale(locale)
    @.ctx.page.language  = getLangcode(locale)
    @.ctx.page.region    = getRegioncode(locale)
  # Stfingify object in case we have not single property
  query = JSON.stringify(query)
  # @note Handle cases, when value starts with `{`, otherwise `JSON.stringify()` will
  #       transform them into inner object
  # @note Handle cases, when we have escaped single quotes inside — we need to enforce
  #       proper escaping for Nunjucks render
  query = query.replace(/\={{/gm, '{{').replace(/\\\\\'/gm, '\\\'')
  rendered = new nunjucks.Template(query).render(@)
  JSON.parse(rendered)

then we can do it like that:
our database (company):
---
type: null
typeFull: null
name: Name
nameFull: Full name
abr: String {{ gettext('gettext string') }}
abrE: ={{ gettext('ge"ttext \'string') }}
details: null {{ test + 32 }}
personnel:
  -
    position: Person
    name:
      first: Name
      last: Last
    photo: null

And our templates:
{% set rendered = dbgettext(company) %}
{% set renderedLocaleSpecific = dbgettext(company, 'ru-UA') %}

Original from db:

{{ company.abr }}

dbgettext from db:

{{ dbgettext(company.abr) }}

From `rendered`:

{{ rendered.abr }}

From `rendered` with escaping:

{{ rendered.abrE }}

From `renderedLocaleSpecific`:

{{ renderedLocaleSpecific.abr }}

Original from gruntfile:

{{ site.twitter }}

dbgettext from gruntfile:

{{ dbgettext(site.twitter) }}

Redefine data with rendered one
{% set company = dbgettext(company) %}

From redefined db:

{{ company.abr }}

Will give us some nice results:
note: render with specific locale returns English strings, since I didn't add translated strings yet. Will test it out later

Some caveats:

You can't write in database like that:
---
abrE: {{ gettext('ge"ttext \'string') }}

JSON.stringify() will thinks it's two inner object and result in improper object for render
That's why I've implemented small hack with leading = which will enforce proper stringification.

So far you can't use double quotes:
---
abrE: ={{ gettext("gettext string") }}

Because it will result in escaped quotes in function gettext(\"gettext string\"), and Nunjucks will throw and error into you
Can be solved by some tricky regex search and replace

Just for consideration, we can also render all databases during preprocessData of grunt-nunjucks-2-html
It will end up like:
---
# `currentLocale` in root
data: 1
company: Test
ru-UA:
	data: 1
	company: Тест
uk-UA:
	data: 1
	company: Тест

Pres:

All data already generated, you can access it without any functions, just like data.company for current locale or data['ru-UA'].company for specific

Cons:

Generated data will be available only in Nunjucks. We still will need to generate it separately to be shareable across other modules. But in most cases Nunjucks is top-consumer of data.
Multiplication of data. With 3 locales — 3x size, with 20 - 20x size. Not an issue in our case, but still doesn't sound good

So far I think it's better to render data on demand with special functions, like dbgettext().

Okey, continuing researching. Let's back to our variant with generated db queries in live on demand.
Actually, what we need to make it full i18n implementation for databases?

Output single standalone string, or mixed with texts, into current locale
Output string with specific context
Output string from specific domain (file in our case)
Output string into specific locale
Placeholders
Output plural strings

1. Ordinary strings
Outputting of standalone or mixed strings works majestically. There is nothing special about it:
---
property:
  subproperty: ={{ gettext('My string 1') }}
  subpropert2: Mixed with `{{ gettext('My string 1') }}` string 2

dbgettext(property.subproperty) # -> 'Моя строка'
dbgettext(property.subproperty2) # -> 'Змішана з `Моя строка 1` строка 2'

2. Around context
Outputting of strings with specific context will work just fine too:
---
property:
  subproperty: ={{ pgettext('text', 'My string') }}
  subproperty2: ={{ pgettext('something else', 'My string') }}

dbgettext(property.subproperty) # -> 'Моя строка'
dbgettext(property.subproperty2) # -> 'Моя строка, але інша'

It works and doesn't require any additional efforts, since we define context inside our property, and there is no situations when you'd like to redefine context from calling dbgettext() function. And you shouldn't do that.
3. Into someone's domain
Outputting of string from specific domain will work too:
---
property:
  subproperty: ={{ dgettext(':navbar', 'My string') }}
  subproperty2: ={{ dgettext(':article', 'My string') }}

dbgettext(property.subproperty) # -> 'Моя строка'
dbgettext(property.subproperty2) # -> 'Моя строка, але інша'

We don't have and shouldn't redefine domain from outer calling function dbgettext(), so there is no problems with it.
Since node-gettext uses domain in a bit inordinary manner (see details here https://github.com/andris9/node-gettext/issues/24), we're using a bit tricky domains-access implementation, described here: https://github.com/LotusTM/Kotsu/commit/c0f1ec0f001120c6b707a6de29b36269027f8cc1
In example above used a bit enhanced version, which treats leading : as "look for that domain in current locale"
4. I wanted to say Sprechen Sie Deutsch?
When we're coming to outputting of strings into specific locale... it becomes a bit worse.
First of all, there is no way for us to control in which language will be outputted gettext(), ngettext() and pgettext(). They're always working with "current locale", which defined in Grunt task during task run.
To enforce outputting of specific locale, we have to use their domain-controlling equivalent, like dgettext() or dngettext().
At first glance there is nothing wrong with it:
---
property:
  subproperty:  ={{ dgettext('en-US', 'My string') }}
  subproperty2: ={{ dgettext('uk-UA', 'My string') }}

dbgettext(property.subproperty) # -> 'My string'
dbgettext(property.subproperty2) # -> 'Моя строка'

That works just fine when we need to have inside our property always translated into specific locale string. You might need it, for example, in such case:
---
property:
  subproperty: Remember, that 'My string' in `uk-UA` should be translated as {{ dgettext('uk-UA', 'My string') }}

But wait... what if we need to output other, not bound to specific locales strings, into another locale?
In other words, we won't face that issue during generation of database for current locale, but it comes into play when we need to output string from specific locale in another locale on demand in view.
For instance, this won't work:
'Some english text with `uk-UA` string insde: ' + dbdgettext('uk-UA', property.subproperty)

Because we have not domain-specific functions inside our property. So, looks like we have to use always domain specific locales in database, but it will be messy anyway:
---
property:
  subproperty: ={{ dgettext(currentLocale, 'My string') }}
  subproperty2: ={{ dgettext(currentLocale, 'My string') }}

Not only we'll have to write much longer dgettext functions inside our database with additional argument, it's unclear where currentLocale variable should come for. It doesn't exist, and we have to create it in context before rendering database with Nunjucks.
By doing this such way, though, we actually can redefine current locale from dbgettext() function:
env.addGlobal 'dbdgettext', (query, locale = currentLocale) ->
  # get context of Nunjucks current environment, which includes whole database
  context = @.getVariables()
  context = _.clone(context)
  # This is where magic happens
  if locale != currentLocale
    context.currentLocale = locale
  # render our query
  # ...

But writing full dgettext() in database properties for no reason (since all regular functions, not bound to specific locale, must be outputable in any locale any time) seems wrong to me.
So far I don't see good solution for it, but this might work:
---
property:
  subproperty: ={{ gettext('My string') }}
  subproperty2: ={{ gettext('My string') }}

env.addGlobal 'dbdgettext', (query, locale = currentLocale) ->
  context = @.getVariables()
  context = _.clone(context)
  tempLocaleName = 'tempGettextLocale'
  # This is where magic happens
  context[tempLocaleName] = locale
  query = JSON.stringify(query)
  # This is where magic happens second time.
  # Replace all regular `gettext()` function with domain-specific functions and our temporary locale
  query = query
    .replace(/gettext(/gm, 'dgettext(' + tempLocaleName + ', ')
    .replace(/ngettext(/gm, 'dngettext(' + tempLocaleName + ', ')
  # ... and other functions ...
  # continue on preparations and render our query
  # ...

So that magically will end up working:
'Some english text with `uk-UA` string insde: ' + dbdgettext('uk-UA', property.subproperty) # -> 'Some english text with `uk-UA` string insde: Моя строка'

The bad side of it that it looks like some kind of black magic, a hack rather then solution... but it works...
5. Hey guys, can you hold that place for me for a minute?
So, the more we're coming to the end, the worse becomes things, aren't they? :) Probably this might indicate that my solution was fault at first place and corrupted in it's nature. But temptation to use single solution for templates, JavaScript and database is so powerful!... Shhh.
On the other hand, it might be me who doesn't see obvious and elegant solution.
What we have now? Placeholders... Situation with placeholder is quite nasty.
First of all, there are two scenarios, when we need to use placeholders:

Static value, which should be pulled into string from somewhere else.

For example, company.name, or site.name — those values actually existing in our database, but sometimes you need to pull them into another string, like this:
---
propety: This is %(company)s site

Dynamic values.

Sometimes we can fill placeholder only when we actually know value, which should be there. And we can't get that value before we'll have needed data for it.
---
propety: This is %(username)s, his color is %(color)s
apples: You have %s apples

We can't fulfil those placeholders with anything, unless we have clear context.
Okey, so, why do we care?
It becomes very important during database rendering, because you suddenly realize that you can't just render database with placeholders, because values for those placeholders are unknown. You will end up with:
---
propety1: ={{ gettext('This is %(sitename)s') }} # will throw error about non-existing placeholder
propety2: ={{ gettext('This is %(username)s, his color is %(color)s') }} # will throw error about non-existing placeholders
apples: ={{ gettext('You have %s apples') }} # will return `You have undefined apples`

You can't pass in placeholder like that:
dbgettext(subproperty1, { sitename: site.name })                       # case 1
dbgettext(subproperty2, { username: result.user. color: result.colo }) # case 2
dbgettext(apples, result.count)                                        # case 3

Because dbgettext() can't pass placeholder as argument inside. And even if it's possible (and let's just say, that it's possible, but tricky and nasty), we'll face following issues anyway:

Case 1 will work in context of template, where you're calling dbgettext() with defined placeholder, but will fail in all other cases. Especially it's important during generation of database for API, or on page, which doesn't have defined dbgettext() with those placeholder. It will end up with undefined values. That's because for obvious reason our database can't know what do you want to place into that particular placeholder, unless you specify it
Cases 2 and 3 awaits same outcome. The only difference here, that we even can't predefine those values somewhere globally, because they're becoming know to us only during some specific operations

For case 1 we can develop some kind of workaround. For example, define globalPlaceholder object, which will hold all placeholders. Then we can pass it throw context and use just like that:
---
propety1: ={{ gettext('This is %(sitename)s'), globalPlaceholder }}

Then you can pass in placeholder like that:
globalPlaceholder = {
  sitename: site.name
}

dbgettext(subproperty1)

So, you will have global placeholders everywhere in database, since it's the only way to define them in context-unaware environment. And, probably, will end up inventing some unclearly named placeholders to avoid collisions.
However, I think it can be completely simplified. Why use global placeholders, if in most cases we're calling back to database? And in context of database I think it shouldn't be made in other way. If we're passing to placeholders values, which database doesn't contain — we're screwed up.
So, it can be just like that:
---
propety1: ={{ gettext('This is %(sitename)s'), sitename. site.name }}

dbgettext(property1) # -> 'This is kotsu'

Those properties rendered in context of Nunjucks, which upholds whole database, so it's completely fine. Nunjucks will serve as a kind of loophole from one property to another. And we don't need ability to redefine them from outer dbgettext() function, since "on going" operation shouldn't affect them, otherwise we won't be able to generate our database for serving in context-unaware environment.
But it doesn't solve case 2 and case 3.
Let's think. If property contains dynamic value, what should it return?
If it's
---
apples: ={{ gettext('You have %s apples') }}

There is no way for us to know how many apples there are to print unless we reach particular line in templates.
So, I think, in such cases gettext('You have %s apples') should return 'You have %s apples'.
Yes, unprocessed placeholder, which we could process with sprintf in view, or where we need. It means, that generated database will uphold such strings with unprocessed placeholders, ready to spit it to us when we need it.
Of course, it's questionable what it should return for APi... but I think that unprocessed string in such case is completely ok too.
Must admit, that I can't say for now for whatever reason you might need to uphold such strings in database, but it's completely possible that they might appear at some point in future.
So far sprintf can't output unprocessed placeholders for undefined values (see details here: https://github.com/alexei/sprintf.js/issues/78). But, as usually, it's possible to preserve them with some voodoo magic and regex.
5. So how many apples do you say you have?
Okey, we're almost done. It was quite long and unpleasant journey. So far we done nothing but built workarounds and hacks. Will it dramatically change in last chapter?
Let's see.
Plurals is one of the most crucial feature of gettext
However, as soon as we try to use it in database — we're in trouble
---
apples: ={{ ngettext('You have %s apple', 'You have %s apples', count, count) }}

Here comes into play at once few issues:

Dynamic values for placeholders — we can't have their values during generation of database
Wow, in fact we can't serve here even some string with unknown placeholders. — we can't know which one plural form to serve

Let's solve it one at once.
1. Dynamic values
Technically it seems to be similar to describe in previous chapter issue.
But it's more complex.
We need possibility to pass in those values from external source. in our case — outer dbgettext(), right during rendering needed content.
But this won't work:
# Let's imagine that we have special `plural` function — `dbngettext()`, like `ngettext`
dbngettext(apples, 2, 2)

This won't work, because there is no clear way to remap those values to inner ngettext() function.
Just to mention, we can't specify it as
---
apples: ={{ ngettext('You have %s apple', 'You have %s apples', %(count)s, %(count)s) }}

Because when you will try to fill %(count)s with sprintf, function already will be rendered and invoked, so ngettext won't be able to choice anything
sprintf(
  dbngettext(apples), { count: 2 } # -> 'You have %(count)s apple'
) # -> 'You have 2 apple'

So you need to do it from outer to inner, which is impossible in that case
We can try to mix our previous approaches:
---
apples: ={{ ngettext('You have %s apple', 'You have %s apples', count) }}

env.addGlobal 'dbngettext', (query) ->
  context = @.getVariables()
  context = _.clone(context)
  # This is where magic happens
  context.count = count
  # continue on preparations and render our query
  # ...

Then you can pass in placeholder like that:
result = dbngettext(apples, 2) # -> 'You have %s apples'
# and finalize with sprintf
sprintf(result, 2)             # -> 'You have 2 apples'

It will work, because dbngettext will pass on argument count as property into database context.
This seems to be ok, but once again — some kind of voodoo magic happening behind. For some reason we're specifying "magically appearing" count variable instead of %(count)s placeholder.
But wait... what if you will pass whole node of database inside function with defined count?
---
list:
  apples: ={{ ngettext('You have %s apple', 'You have %s apples', count) }}
  sub:
    car: ={{ ngettext('You have %s car', 'You have %s cars', count) }}

sprintf(dbngettext(list, 2), 2)

will result it:
---
list:
  apples: You have 2 apples
  sub:
    car: You have 2 cars

Oops... probably not what we expected...
But I guess it's improper to use function with specified count on nodes, unless you really want all count to be filled with provided value?
2. Plural forms and undefined placeholders
In case of undefined placeholders, we can't return here same string, because some language can have two plural forms, some — 3 or more.
So, what string we should return here? We can't just return 'You have %s apple', because then we might want to provide here 5 for placeholder, and... ops, it results in 'You have 5 apple'
So, for cases, when we have unknown placeholders, we need to return array of possible values:
---
apples:
  - 'You have %s apple'
  - 'You have %s apples'

Then we could use smartPlurals to select proper form based on count, and sprintf to fill placeholder with necessary value. I don't like involving of 3rd party utility for pluralization here, but I don't see another option so far.
In case we know placeholders values, we can return pluralized string as usually:
---
apples: ={{ ngettext('You have %s apple', 'You have %s apples', 2, 2) }}

dbgettext(apples) # -> 'You have 2 apples'

But this have few caveats too:

Technically you can't have ngettext() template mixed with string, because script won't know where to fill in array in case of unknown placeholders. But it's only in case of unknown placeholder's value.

But if you know those values right inside database — why do you even use ngettext()? Unless you need loophole to another database property with number...

ngettext() can't decide, what should it do — return array or localized string in case of unknown count or not.

It's possible to solve by augmenting ngettext():
env.addGlobal 'ngettext', (string, stringPlural, count, placeholders...) ->
  if count
    # return usual `ngettext()`
  else
    # return array with possible values

It's a bit tricky to get all plural forms, but it's possible by accessing node-gettext plural rules for particular locale (see here: https://github.com/andris9/node-gettext/issues/25), or getting possible values from gt.domains[*locale name*]...[*array with plural forms*]

Praise the Summarization!
What we have in the end?

Output single standalone string, or mixed with texts, into current locale

Works, almost no tricks.
Leading = or wrapping quotes for cases, when there it's not mixed with other strings
---
property1: With text {{ gettext('My string') }}
property2: ={{ gettext('My string') }}
property3: "{{ gettext('My string') }}"
property4: '{{ gettext(\'My string\') }}'

Inner double qoutes for arguments won't work — need some tricky regext to properly escape them:
---
property4: '{{ gettext("My string") }}' # won't work
property4: '{{ gettext(\"My string\") }}' # won't work
property4: "{{ gettext(\"My string\") }}" # won't work

Output string with specific context

Just works, like regular strings. We don't need to redefine context in outer function.

Output string from specific domain (file in our case)

Just works, like regular strings. We don't need to redefine domain in outer function

Output string into specific locale

A bit of replacing regex, and redefining with outer function works:
---
property: ={{ gettext('My string') }}

'Some english text with `uk-UA` string insde: ' + dbdgettext('uk-UA', property)

It will generate all non-language specific functions with specified language.

Placeholders

You can predefine placeholders in database, using them as kind of loophole or micro-templating for accessing other database values:
---
propety1: ={{ gettext('This is %(sitename)s'), { sitename: site.name }}

dbgettext(property1) # -> 'This is kotsu'

But you can't define those placeholders elsewhere. On that point we're assuming that you would never do it.
Undefined placeholders will be returned as they are, so you can template them with sprintf (|template() Nunjucks filter in Kotsu) in view:
---
propety1: ={{ gettext('This is %(sitename)s') }}

{{ dbgettext(property1)|template({ sitename: 'My name' }) # -> 'This is My name' }}

This is how you can define placeholders "on demand"

Output plural strings

You need to use a bit "magic" count variable inside database property, which will pass on value from outer function dbngettext():
---
apples: ={{ ngettext('You have %s apple', 'You have %s apples', count) }}

dbngettext(apples, 2)|template(2) # -> 'You have %s apples'

In case of undefined count, array of possible forms will be returned for further templating:
---
apples:
  - 'You have %s apple'
  - 'You have %s apples'

{{ apples|plural(2)|template(2) } # -> 'You have %s apples'

You can't have mixed ngettext with string inside single property, otherwise output might be unpredictable.
Only one count (and previous limitation) makes it impossible to use more then one ngettext in single property
Calling dbngettext(database, 2) with defined count will result in node, where every count will be filled with defined number, and ngettext() rendering according to that number


Health Score Not getting updated.

Description of the bug
Hi Team,
Health score not getting updated though all conditions are met for Secone Security Plugin.
link for the plugin:  https://plugins.jenkins.io/secone-security/healthscore/
PFA screenshot of plugin page. There are no PR which are in open state but still out score is 50% for Pr section.
"Dependabot is configured.
2 open dependency pull request"
let us know if you require more information.

Already tracked in #522


extasy_gromacs_lsdmap documentation

There are 12 files in usecases/extasy_gromacs_lsdmap directory. Maybe these should be organised somehow?
pre_analyze.py - no comments at all
post_analyze.py - no comments at all
spliter.py - no comments at all
select.py - needs more comments
run.py - needs more comments
What are the relationships between these files? If there are just two stages, e.g. simulation analysis why there are seven python files? merge them?
01_static_gromacs_lsdmap_loop.py:
line 57: # TODO Vivek: add description. ?
pre_loop() - why not RP shared data routines?
So if 'numCUs' is the number of simulation instances per iteration and 'nsave' is the iteration at which backup needs to be created on the local machine how can I know how many iterations this script actually performs?
How can I change the number of iterations?
Why on 16 cores we are launching 8 CU's per iteration?

line188  # We set the 'instances' of the simulation step to 16. but this is hardcoded as num_CUs = 8

If I use RADICAL_ENMD_PROFILING=1:
Total simulation time is not provided
Time for analysis step is not provided
Time for simulation step is not provided

description of files and individual parameters has been added in devel. Profiling has been addressed by Ole in the documentation.


Scaling out with thousands of devices

How does this approach scale out when using thousands of devices, each with unique threshold requirements?  I don't think retrieving a json file consisting of thousands of threshold settings from blob storage for each analytics query would be very efficient.

Document Details
⚠ Do not edit this section. It is required for docs.microsoft.com ➟ GitHub issue linking.

ID: 1e5c46b1-3679-7fdc-46e2-b22d66133610
Version Independent ID: 1eb5c908-346f-0fe0-3eec-f4131ae7e05f
Content: Process configurable threshold based rules in Azure Stream Analytics
Content Source: articles/stream-analytics/stream-analytics-threshold-based-rules.md
Service: stream-analytics
GitHub Login: @zhongc
Microsoft Alias: zhongc

@greyghostbrewery Thanks for the feedback! I have assigned the issue to the content author to evaluate and update as appropriate.

Is someone going to respond to this?

ASA query is a standing query, meaning it’s only loaded once, along with the reference data, when the query starts. It then runs without reload, until a newer version of the reference data is available.

@greyghostbrewery I hope that answers your question. We will now proceed to close this thread. If there are further questions regarding this matter, please reopen it and we will gladly continue the discussion.


How to find force of body?

Hello.
I need to sum speed of two bodies one of which is emitter (a moving missle launcher for example). Because only way to set speed of body is by applying force to it, i need to find force of first body and add it to second body.
I was found a formula: http://formulas.tutorvista.com/physics/force-formula.html that says i need to know acceleration which is defined as velocity per time. I have velocity but does not have time and i'm actually don't want to have reference to engine.timing inside my entity instance because it's ruins separation of concerns and lead to spaghetti code.
Is there any way to find acceleration or force of body or maybe you can add a Body.applyBodyForce(bodyA, bodyB) helper for this?

Because only way to set speed of body is by applying force to it

Actually in the latest master build there is Body.setVelocity, does that help?

Yes. It's exactly what i'm looking for. Thanks.


Examples: remove Thundra APM as it's out of service

Reference: https://www.catchpoint.com/press-releases/catchpoint-invests-to-advance-api-cloud-functions-and-microservices-monitoring

Thank you!


nil pointer dereference with kuttl 0.4.0

I've just run into the following nil pointer crash and thought I'd report it:
    kuttl/harness/registry: logger.go:42: 17:25:15 | registry | Creating namespace: kudo-test-poetic-troll
    kuttl/harness/registry: logger.go:42: 17:25:15 | registry/0-create | starting test step 0-create
    kuttl/harness/registry: logger.go:42: 17:25:15 | registry/0-create | running command: "helm init --client-only"
    kuttl/harness/registry: logger.go:42: 17:25:15 | registry/0-create | running command: [helm init --client-only]
    kuttl/harness/registry: logger.go:42: 17:25:15 | registry/0-create | $HELM_HOME has been configured at /tekton/home/.helm.
    kuttl/harness/registry: logger.go:42: 17:25:15 | registry/0-create | Not installing Tiller due to 'client-only' flag having been set
    kuttl/harness/registry: logger.go:42: 17:25:15 | registry/0-create | running command: "helm delete --purge docker-registry"
    kuttl/harness/registry: logger.go:42: 17:25:15 | registry/0-create | running command: [helm delete --purge docker-registry]
    kuttl/harness/registry: logger.go:42: 17:25:26 | registry/0-create | Error: release: "docker-registry" not found
...
    kuttl/harness/registry: logger.go:42: 17:26:52 | registry/0-create | Error: release docker-registry failed: Get https://<IP_ADDRESS>:443/api/v1/namespaces/kudo-test-poetic-troll: dial tcp <IP_ADDRESS>:443: connect: connection refused
    kuttl/harness/registry: case.go:156: exit status 1
    kuttl/harness/registry: case.go:156: Get "https://<IP_ADDRESS>:32768/api?timeout=32s": EOF
    kuttl/harness/registry: case.go:159: failed in step 0-create
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x131e945]

goroutine 8260 [running]:
github.com/kudobuilder/kuttl/pkg/test/utils.(*RetryClient).List.func1(0x1936320, 0xc000046048, 0x0, 0x0)
	/Users/kensipe/projects/go/src/github.com/kudobuilder/kuttl/pkg/test/utils/kubernetes.go:228 +0x25
github.com/kudobuilder/kuttl/pkg/test/utils.Retry.func1(0xc0004670c0, 0x1936320, 0xc000046048, 0xc0006d5b60, 0xc0006d5ce0)
	/Users/kensipe/projects/go/src/github.com/kudobuilder/kuttl/pkg/test/utils/kubernetes.go:110 +0x3e
created by github.com/kudobuilder/kuttl/pkg/test/utils.Retry
	/Users/kensipe/projects/go/src/github.com/kudobuilder/kuttl/pkg/test/utils/kubernetes.go:109 +0xf7
Collecting kind cluster logs.

I believe this was with kuttl version 0.4.0.

hmm... this is clearly a nil being passed in as a func.  Test confirm as well.
It is easy to defend against this... but it is hard to track down the function that passed a nil.  It is clearly a List  which means r.Client.List.   This is in a go routine... it is easily possible that the client was torn down on the main thread prior to this thread running.
tough problem!  thanks for logging this!   And let this be a lesson around passing a reference to go routine which stays in a forever loop :)
All code review was on v0.4.0.   I'll add a fix here for a v0.4.1, v0.5.2, then add for v0.6.1 and master.

not as easy as I hoped...  the issue is the fn is an anonymous function which is of course not nil... it has calls internal which when called are nil.   We will have a solution soonish with tests.


Form group on the “My Health Log Symptoms List” screen is not programmatically associated with its group label [iOS]

The “Do you have any of the following 11 symptoms today?” form group label is not announced upon focusing a control in the form group. For example, the “Fatigue” checkbox is announced as “Fatigue, unselected, checkbox”.

Use the accessibilityLabel property to include the group label in the accessible name of each control. Remove the group label from the reading order using the accessibilityElementsHidden, as it is redundant when included in the accessibilityLabel of each checkbox.
<View accessibilityElementsHidden={true}>
  <Text>Do you have any of the following 11 symptoms today?</Text>
</View>

<TouchableOpacity
  onPress={…}
  accessibilityLabel='Fatigue, Do you have any of the following 11 symptoms today?'
  accessibilityRole='checkbox'
  accessibilityState={{selected: false}}
>

@willslone This has now been addressed


How to inject Interceptors programmatically

Hi,
I want to add Out Interceptors programmatically by class name. Is there a way to do so? I did not find any clue is the documentation.
Thanks

Is this not helpful? https://docs.quarkiverse.io/quarkus-cxf/dev/user-guide/cxf-interceptors-and-features.html

@mcanzerini please feel free to reopen when the above resource does not help.


Forward audio

Last month, @yume-chan submitted a working PoC to capture audio on Android. Big thanks for this discovery!
Since then I have worked to properly integrate audio support in scrcpy (capture, encode, transmit, record, decode, resample, play…), and my branch reached a working state (at least on my machine) :tada:
By default, audio is enabled (if supported):

On Android >= 12, it works out-of-the-box;
On Android 11, the device screen must be unlocked (because of a dirty workaround to make the system think the app is foreground, again thank you to @yume-chan for the tip);
On Android <= 10, it does not work, audio is automatically disabled.

If audio capture fails, then mirroring continues with video only (since audio is enabled by default, it is not acceptable to make scrcpy fail if audio is not available), unless --require-audio is set.
To disable audio:
scrcpy --no-audio

If audio is enabled, it is also recorded when recording is enabled:
scrcpy --record=file.mp4
scrcpy --record=file.mkv

To play audio in real-time, the audio player implements drift compensation (it attempts to keep audio sample buffering to a certain level, quite low to get low-latency, but not too low to avoid underflow). It is experimental ~(I just tinkered with this thing today)~ (I've made a lot of improvements since the initial MR, and it should work pretty fine), parameters and behavior are hardcoded, some configuration variables might need to be exposed in the future (or not).
If you record, the timestamps are computed on the device, so they are not impacted by any jitter/underflow (this is the same as for the video), so the recording is always clean (if you use --record of course, not if you capture your audio output on the computer).
Like the video, it works over TCP/IP (typically Wifi).
The following options have been renamed:

--codec -> --video-codec
--bit-rate -> --video-bitrate
--codec-options -> --video-codec-options
--encoder -> --video-encoder

And similar options for audio have been added:

--audio-codec
--audio-bit-rate
--audio-codec-options
--audio-encoder

Three audio formats are supported:

Opus: scrcpy --audio-codec=opus
AAC: scrcpy --audio-codece=aac
Raw (uncompressed) PCM 16-bit LE: scrcpy --audio-codec=raw (more bandwidth, typically just for testing)

It is possible to list the encoders available on the device (video and audio):
scrcpy --list-encoders

If you need a specific encoder, use --audio-encoder:
scrcpy --audio-codec=aac --audio-encoder='OMX.google.aac.encoder'

When recording, raw audio is not possible (it automatically switches to OPUS). In the future, I might add audio encoding on the client-side specifically for recording, but I don't plan to make it for this audio feature.
I also implemented --audio-buffer= similar to existing --display-buffer= and --v4l2-buffer=.
This branch is in working state.
Since I have refactored A LOT of code, I may have broken things (please tell me!).
Please test, review (good luck, that's a big PR :wink:), and report any problem :)
Fixes #14

old
Here is a release build for the current MR for Windows (audio.115):

scrcpy-win64-PR3757-1.zip SHA-256: 8a46774889f0d6602e7f94c1e664b7184fdc0f0ccfe4d114045d2a8eefb408f6
scrcpy-win32-PR3757-1.zip SHA-256: 7b07819377fa87f1a94f5a424b6757590492b32309d59457fb4eb1e543902bd4

Here is a release build for the current MR for Windows (audio.145):

scrcpy-win64-PR3757-1.zip SHA-256: 6ef3c45292f5c3b713628e593bbf695be586506cd38a26f038d135cd88d08033
scrcpy-win32-PR3757-1.zip SHA-256: 06fc50e289494554aa1d86a783960b5fd42268ac18395f224b0ea355aa377dd5

Are the BUILD instructions outdated?
(deck@steamdeck build)$ git clone https://github.com/Genymobile/scrcpy
Cloning into 'scrcpy'...
remote: Enumerating objects: 23178, done.
remote: Counting objects: 100% (3736/3736), done.
remote: Compressing objects: 100% (921/921), done.
remote: Total 23178 (delta 2443), reused 3435 (delta 2189), pack-reused 19442
Receiving objects: 100% (23178/23178), 4.74 MiB | 9.34 MiB/s, done.
Resolving deltas: 100% (14698/14698), done.
(deck@steamdeck build)$ cd scrcpy/
(deck@steamdeck scrcpy)$ git checkout audio
branch 'audio' set up to track 'origin/audio'.
Switched to a new branch 'audio'
(deck@steamdeck scrcpy)$ meson setup x --buildtype=release --strip -Db_lto=true
The Meson build system
Version: 1.0.1
Source dir: /home/deck/build/scrcpy
Build dir: /home/deck/build/scrcpy/x
Build type: native build
Project name: scrcpy
Project version: 1.25
C compiler for the host machine: cc (gcc 12.2.0 "cc (GCC) 12.2.0")
C linker for the host machine: cc ld.bfd 2.39.0
Host machine cpu family: x86_64
Host machine cpu: x86_64
Found pkg-config: /usr/bin/pkg-config (1.8.0)
Run-time dependency libavformat found: YES 59.27.100
Run-time dependency libavcodec found: YES 59.37.100
Run-time dependency libavutil found: YES 57.28.100
Run-time dependency libswresample found: YES 4.7.100
Run-time dependency sdl2 found: YES 2.24.1
Run-time dependency libavdevice found: YES 59.7.100
Run-time dependency libusb-1.0 found: YES 1.0.26
Checking for function "strdup" : YES
Checking for function "asprintf" : YES
Checking for function "vasprintf" : YES
Checking for function "nrand48" : YES
Checking for function "jrand48" : YES
Header "sys/socket.h" has symbol "SOCK_CLOEXEC" : YES
Configuring config.h using configuration

app/meson.build:226:0: ERROR: File src/util/average.c does not exist.

A full log can be found at /home/deck/build/scrcpy/x/meson-logs/meson-log.txt

Seems to be nowhere int he sources? I don't even see a src folder
(deck@steamdeck scrcpy)$ find . -name "average.c"
(deck@steamdeck scrcpy)$

app/meson.build:226:0: ERROR: File src/util/average.c does not exist.

Oops, it was not committed, sorry. Fixed.

ninja: Entering directory `x'
[48/59] Generating server/scrcpy-server with a custom command

FAILURE: Build failed with an exception.

* What went wrong:
Could not open settings generic class cache for settings file '/home/deck/build/scrcpy/settings.gradle' (/home/deck/.gradle/caches/7.5/scripts/4g6ucengw406w5p3xne7pf3a4).
> BUG! exception in phase 'semantic analysis' in source unit '_BuildScript_' Unsupported class file major version 63

* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.

* Get more help at https://help.gradle.org

BUILD FAILED in 752ms
[57/59] Linking target app/scrcpy
FAILED: server/scrcpy-server
/home/deck/build/scrcpy/server/./scripts/build-wrapper.sh /home/deck/build/scrcpy/server server/scrcpy-server release
[59/59] Linking target app/scrcpy
ninja: build stopped: subcommand failed.

Aha java is too new (jdk 19). Trying an older version

Looking forward to this PR!

I improved the audio player (the drift compensation to keep a minimal both latency and underflow).
I added support for RAW audio (PCM 16-bit LE) in addition to OPUS and AAC. RAW audio is now the default: it consumes more bandwidth, but provides a lower latency.
Encoders (for both OPUS and AAC) typically produce blocks of ~20ms, so it adds 20ms latency on the encoder side (the device) and 20ms additional latency on the decoder side (the computer). With raw audio, I can send blocks of 5ms (so it's 30ms lower, + the encoding/decoding duration, probably negligible).
However, if recording is enabled, raw audio is not possible (it automatically switches to OPUS). In the future, I might add audio encoding on the client-side specifically for recording, but I don't plan to make it for this audio feature.
I also implemented --audio-buffer= similar to existing --display-buffer= and --v4l2-buffer=.
Since the audio feature is almost ready, I published binaries for Windows (in the original post), so that people can test easily.
Please test and give feedback (and report any bug or remark).

There is a small amount of weirdness when the audio has to catch up but overall it works super well!

It didn't work on my vivo Android 12 device.
D:\adb\scrcpy-win64-PR3757-1\scrcpy-win64-v1.25-183-gd939b99dd>scrcpy --audio-codec=aac -Vdebug
scrcpy 1.25 https://github.com/Genymobile/scrcpy
DEBUG: ADB device found:
DEBUG:     -->   (usb)<PHONE_NUMBER>003D0            device  V2157A
DEBUG: Device serial:<PHONE_NUMBER>003D0
DEBUG: Using server (portable): D:\adb\scrcpy-win64-PR3757-1\scrcpy-win64-v1.25-183-gd939b99dd\scrcpy-server
D:\adb\scrcpy-win64-PR3757-1\scrcpy-win64-v1.25-183-gd939b... file pushed, 0 skipped. 52.7 MB/s (54035 bytes in 0.001s)
[server] INFO: Device: vivo V2157A (Android 12)
[server] DEBUG: Using audio encoder: 'c2.android.aac.encoder'
[server] DEBUG: Using encoder: 'OMX.qcom.video.encoder.avc'
DEBUG: Server connected
DEBUG: Starting controller thread
DEBUG: Starting receiver thread
[server] DEBUG: Audio encoder stopped
INFO: Renderer: direct3d
DEBUG: Trilinear filtering disabled (not an OpenGL renderer)
DEBUG: Using icon (portable): D:\adb\scrcpy-win64-PR3757-1\scrcpy-win64-v1.25-183-gd939b99dd\icon.png
INFO: Initial texture: 1080x2400
DEBUG: Demuxer 'video': starting thread
DEBUG: Demuxer 'audio': starting thread
WARN: Demuxer 'audio': stream explicitly disabled by the device
[server] ERROR: Exception on thread Thread[Thread-4,5,main]
java.lang.NullPointerException: Attempt to invoke virtual method 'java.lang.String android.content.Context.getOpPackageName()' on a null object reference
at android.media.VivoAudioRecordImpl.isSupportSubMixRecording(VivoAudioRecordImpl.java:133)
at android.media.AudioRecord.(AudioRecord.java:497)
at android.media.AudioRecord.(AudioRecord.java:91)
at android.media.AudioRecord$Builder.build(AudioRecord.java:935)
at com.genymobile.scrcpy.AudioCapture.createAudioRecord(AudioCapture.java:59)
at com.genymobile.scrcpy.AudioCapture.start(AudioCapture.java:91)
at com.genymobile.scrcpy.AudioEncoder.encode(AudioEncoder.java:183)
at com.genymobile.scrcpy.AudioEncoder.lambda$start$0$com-genymobile-scrcpy-AudioEncoder(AudioEncoder.java:120)
at com.genymobile.scrcpy.AudioEncoder$$ExternalSyntheticLambda0.run(Unknown Source:2)
at java.lang.Thread.run(Thread.java:922)

@Helaer Could you please test this change (replace the file scrcpy-server in your scrcpy folder):

scrcpy-server SHA-256: c86353e6040fb65d91ff686d6cd374a809242256d1ad6383c003c3c958621bd0

diff
diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java b/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java
index ff5c0a7f6..a6fa8b053 100644
--- a/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java
+++ b/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java
@@ -47,6 +47,7 @@ public final class AudioCapture {
     @SuppressLint({"WrongConstant", "MissingPermission"})
     private static AudioRecord createAudioRecord() {
         AudioRecord.Builder builder = new AudioRecord.Builder();
+        Ln.i("Build.VERSION.SDK_INT = " + Build.VERSION.SDK_INT);
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
             // On older APIs, Workarounds.fillAppInfo() must be called beforehand
             builder.setContext(FakeContext.get());
diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java
index a80a8cd3a..0b01e93a0 100644
--- a/server/src/main/java/com/genymobile/scrcpy/Server.java
+++ b/server/src/main/java/com/genymobile/scrcpy/Server.java
@@ -85,7 +85,7 @@ public final class Server {
         // Before Android 11, audio is not supported.
         // Since Android 12, we can properly set a context on the AudioRecord.
         // Only on Android 11 we must fill app info for the AudioRecord to work.
-        mustFillAppInfo |= audio && Build.VERSION.SDK_INT == Build.VERSION_CODES.R;
+        mustFillAppInfo |= audio && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R;

         if (mustFillAppInfo) {
             Workarounds.fillAppInfo();

Seems to work fine for me on android 13 but the audio is not forwarded when using otg mode.

That's expected. OTG mode works without adb or server installed on the device (it just acts as a USB mouse or keyboard), so it could not capture audio.

Maybe some kind of hybrid mode?

You can use what OTG provides while mirroring: https://github.com/Genymobile/scrcpy#physical-keyboard-simulation-hid

scrcpy --audio-codec=aac

It still doesn't work.
D:\adb\scrcpy-win64-v1.25-183-gd939b99dd>scrcpy --audio-codec=aac -Vdebug
scrcpy 1.25 https://github.com/Genymobile/scrcpy
DEBUG: ADB device found:
DEBUG:     -->   (usb)<PHONE_NUMBER>003D0            device  V2157A
DEBUG: Device serial:<PHONE_NUMBER>003D0
DEBUG: Using server (portable): D:\adb\scrcpy-win64-v1.25-183-gd939b99dd\scrcpy-server
D:\adb\scrcpy-win64-v1.25-183-gd939b99dd\scrcpy-server: 1 file pushed, 0 skipped. 133.7 MB/s (131150 bytes in 0.001s)
[server] INFO: Device: vivo V2157A (Android 12)
[server] DEBUG: Using encoder: 'OMX.qcom.video.encoder.avc'
[server] DEBUG: Using audio encoder: 'c2.android.aac.encoder'
[server] INFO: Build.VERSION.SDK_INT = 31
DEBUG: Server connected
DEBUG: Starting controller thread
DEBUG: Starting receiver thread
[server] DEBUG: Audio encoder stopped
[server] ERROR: Exception on thread Thread[Thread-4,5,main]
java.lang.NullPointerException: Attempt to invoke virtual method 'android.content.pm.PackageManager android.content.Context.getPackageManager()' on a null object reference
at android.content.ContextWrapper.getPackageManager(ContextWrapper.java:105)
at android.content.ContextWrapper.getPackageManager(ContextWrapper.java:105)
at android.media.VivoAudioRecordImpl.getSignatures(VivoAudioRecordImpl.java:101)
at android.media.VivoAudioRecordImpl.getSignInfo(VivoAudioRecordImpl.java:68)
at android.media.VivoAudioRecordImpl.isSupportSubMixRecording(VivoAudioRecordImpl.java:134)
at android.media.AudioRecord.(AudioRecord.java:497)
at android.media.AudioRecord.(AudioRecord.java:91)
at android.media.AudioRecord$Builder.build(AudioRecord.java:935)
at com.genymobile.scrcpy.AudioCapture.createAudioRecord(AudioCapture.java:60)
at com.genymobile.scrcpy.AudioCapture.start(AudioCapture.java:92)
at com.genymobile.scrcpy.AudioEncoder.encode(AudioEncoder.java:183)
at com.genymobile.scrcpy.AudioEncoder.lambda$start$0$com-genymobile-scrcpy-AudioEncoder(AudioEncoder.java:120)
at com.genymobile.scrcpy.AudioEncoder$$ExternalSyntheticLambda0.run(Unknown Source:2)
at java.lang.Thread.run(Thread.java:922)
INFO: Renderer: direct3d
DEBUG: Trilinear filtering disabled (not an OpenGL renderer)
DEBUG: Using icon (portable): D:\adb\scrcpy-win64-v1.25-183-gd939b99dd\icon.png
INFO: Initial texture: 1080x2400
DEBUG: Demuxer 'video': starting thread
DEBUG: Demuxer 'audio': starting thread
WARN: Demuxer 'audio': stream explicitly disabled by the device

@Helaer OK, t's not the same issue (so we avoided the first exception).
But now, that's more complicated, because we don't have any instance of PackageManager (and it's not easy to get one without real Context). It's easy to get an IPackageManager (ActivityThread.getPackageManager()), but then to get a PackageManager we may create an ApplicationPackageManager, but this require a ContextImpl (which we don't have).
https://github.com/aosp-mirror/platform_frameworks_base/blob/cd2bdd1e843a92825bdf85a318c7661741af6b49/core/java/android/app/ContextImpl.java#L392-L405
So I probably won't get a solution for this device. That's annoying device ROMs which do anything, starting an AudioRecord should not call a PackageManager… Apparently, they check some signatures, so they probably deny recording "audio submix", so record would probably fail anyway.

Yes, -KM

I added support for RAW audio (PCM 16-bit LE) in addition to OPUS and AAC. RAW audio is now the default: it consumes more bandwidth, but provides a lower latency.
MediaCodec encoders (for both OPUS and AAC) typically produce blocks of ~20ms, so it adds 20ms latency on the encoder side (the device) and 20ms additional latency on the decoder side (the computer). With raw audio, I can send blocks of 5ms (so it's 30ms lower, + the encoding/decoding duration, probably negligible).

In fact, this is not true.
Firstly, RAW audio takes more time to be transmitted (15~20ms according to my measurements, I made a hack to write the send date in the PTS, and when received and decoded I write back the PTS to a socket, to compare on the device from the same clock source) compared to opus.
Secondly, the 20ms minimal latency on Android is not limited to encoders, it is also a limitation of the audio capture (AudioRecord always produces audio every 20ms, so it's an incompressible latency AFAIK :disappointed:). As a consequence, the encoder will always receive blocks of 20ms, so it does not have to wait more (in other words, its latency is "absorbed" by the latency of the capture).
Therefore, I make OPUS the default. I keep --audio-codec=raw now that it is implemented, but it's not very useful.

Stop force pushing, please...... It will make all conversations way up there

Yes, discussions in GitHub PR are unusable when there are a lot of commits. :/

It's not even the force push only, it's because many commits reference the PR in the commit message, so even if I push to a separate branch, it adds a lot of noise here :disappointed: I will keep my branches locally (I have still many things to rework regarding the audio player).

Hi,
Here are binaries for the latest version:

scrcpy-win64-PR3757-3.zip SHA-256: 6ef3c45292f5c3b713628e593bbf695be586506cd38a26f038d135cd88d08033
scrcpy-win32-PR3757-3.zip SHA-256: 06fc50e289494554aa1d86a783960b5fd42268ac18395f224b0ea355aa377dd5

Please check the new documentation (in particular the Audio page): https://github.com/Genymobile/scrcpy/pull/3774
Your feedback is very welcome. Please report any problem (and also if it works well, that's reassuring). Consider these binaries almost as a pre-release :wink:

Android 12/13 still working well on my end. May be imagining it but does seem a lot "smoother" on the audio. Don't have the occasional clips.
:+1:

May be imagining it but does seem a lot "smoother" on the audio

It should :)
Also does adding manual buffering work fine? For example 1 second:
scrcpy --display-buffer=1000 --audio-buffer=1000

Is the lipsync ok?

Both real time and delayed look fine to my eyes.

When watching a youtube video, If I click on the fullscreen button, the audio breaks and distorts while the screen is rotating.

Does it only happen on youtube? Reproducible 100℅? If you retest with the old binary (it's still linked in the thread), does it happen? If you record, is the glitch recorded?

It still doesn't work on my vivo phone, so it looks like the problem can't be fixed.

It still doesn't work on my vivo phone, so it looks like the problem can't be fixed.

Have you tried grabbing a logcat to see what's going wrong?

And the audio glitch does not get recorded.

Does it happen with higher buffering?
scrcpy --audio-buffer=500 --display-buffer=500

Let's merge this.
I will continue some work on the dev branch, but I can't keep a working branch with 118 commits. Basically, it works :wink:

@rp1231 I reference previous commits since the github thread is unusable: https://github.com/Genymobile/scrcpy/pull/3757#issuecomment-1463044492 https://github.com/Genymobile/scrcpy/pull/3757#issuecomment-1464052773

But I've noticed a small problem in this version which didn't exist in the previous version,
When watching a youtube video, If I click on the fullscreen button, the audio breaks and distorts while the screen is rotating.

Is the mod+r Android 11 bug fixed in this commit or will it be fixed in the next one?

It is not fixed.
Since the glitch is not recorded and is reduced with more buffering, here is my hypothesis.
When buffer underflow occurs, the player inserts silence samples (it does not have any valid samples yet). Typically, buffer underflow occurs because some packets are late (due to jitter), so they are expected to arrive, but later. Therefore, inserting silence will delay the playback of future packets, and therefore I immediately shift the buffering level average by the number of silence samples inserted. If I didn't do that, then the overbuffering due to the inserted silence will be detected slowly (because the average buffering is smoothed), so it will first accelerate the playback (because underflow), then decelerate (because overbuffering will finally be detected).
But there might be another cause of buffer underflow, which is currently not taken into account at all: when the device just misses to capture samples for a period of time. This is less likely than jitter, but I suppose that this is what happens when you rotate your device during fullscreen youtube playback: the device "freezes" and don't capture audio for a short period.
In that case, considering that the missing samples will arrive (and count them for the buffering level) is a wrong prediction, which will be corrected "slowly" by the smoothed (observed) buffering level average. During this time, more underflow will occur.
One possible fix could be to use the input packet PTS to detect that samples have not been captured by the device, but another issue makes it impossible: the default OPUS encoder writes its own timestamps in the encoded packets it produces, in a way that it matches the number of actual samples (it does not use the input timestamps provided by the audio capture), so missing packets could not be detected.
The AAC encoder does not do that though (it forwards the input timestamp), so I build a specific version with more logs, to confirm my hypothesis.

scrcpy-win64-PR3757-rp1231.zip SHA-256: 5840c2b4075724d740acdcf6a2afa7f777efe80470649a6b04cff4b57eca5935

Please run it with:
scrcpy --audio-codec=aac --Vdebug

diff
diff --git a/app/src/audio_player.c b/app/src/audio_player.c
index 41e7565f2..ed0934691 100644
--- a/app/src/audio_player.c
+++ b/app/src/audio_player.c
@@ -144,6 +144,17 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink,
                                 const AVFrame *frame) {
     struct sc_audio_player *ap = DOWNCAST(sink);

+    assert(frame->pts != AV_NOPTS_VALUE);
+    if (ap->expected_next_pts != AV_NOPTS_VALUE) {
+        int64_t diff = ap->expected_next_pts - frame->pts;
+        if (diff) {
+            LOGI("==== unexpected PTS diff = %" PRIi64, diff);
+        }
+    }
+    int64_t frame_duration_us = INT64_C(1000000) * frame->nb_samples
+                              / ap->sample_rate;
+    ap->expected_next_pts = frame->pts + frame_duration_us;
+
     SwrContext *swr_ctx = ap->swr_ctx;

     int64_t swr_delay = swr_get_delay(swr_ctx, ap->sample_rate);
@@ -414,6 +425,7 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink,
     ap->received = false;
     ap->played = false;
     ap->underflow = 0;
+    ap->expected_next_pts = AV_NOPTS_VALUE;

     // The thread calling open() is the thread calling push(), which fills the
     // audio buffer consumed by the SDL audio thread.
diff --git a/app/src/audio_player.h b/app/src/audio_player.h
index 3227f2fef..224abe62d 100644
--- a/app/src/audio_player.h
+++ b/app/src/audio_player.h
@@ -35,6 +35,9 @@ struct sc_audio_player {
     // thread)
     uint32_t previous_can_write;

+    // Useful to detect missing packets (e.g. due to a device freeze)
+    int64_t expected_next_pts;
+
     // Resampler (only used from the receiver thread)
     struct SwrContext *swr_ctx;

The version I build in the previous comment was on a branch with a major bug for estimation. Here is a new binary:

scrcpy-win64-PR3757-rp1231-2.zip SHA-256: 16d713e26068182e62a7886bd0490f9276abaaa744573014a7bd43580542311e

Also, please run with -Vverbose instead of -Vdebug as I suggested:
scrcpy --audio-codec=aac --Vverbose

It is -Vverbose (single dash).

that this problem didn't occur in the very first build you had posted

Ok, but that could a side effect that in this first build buffering might be higher than expected.

I could reproduce on a tablet:
INFO: ==== unexpected PTS diff = 0 samples
INFO: New texture: 1200x1920
INFO: ==== unexpected PTS diff = 0 samples
INFO: ==== unexpected PTS diff = 0 samples
DEBUG: [Audio] Buffer underflow, inserting silence: 46 samples
DEBUG: [Audio] Buffer underflow, inserting silence: 240 samples
DEBUG: [Audio] Buffer underflow, inserting silence: 240 samples
DEBUG: [Audio] Buffer underflow, inserting silence: 240 samples
DEBUG: [Audio] Buffer underflow, inserting silence: 240 samples
DEBUG: [Audio] Buffer underflow, inserting silence: 240 samples
DEBUG: [Audio] Buffer underflow, inserting silence: 240 samples
DEBUG: [Audio] Buffer underflow, inserting silence: 240 samples
INFO: ==== unexpected PTS diff = -313 samples
INFO: ==== unexpected PTS diff = -554 samples
DEBUG: [Audio] Buffer underflow, inserting silence: 239 samples
DEBUG: [Audio] Buffer underflow, inserting silence: 240 samples
DEBUG: [Audio] Buffer underflow, inserting silence: 240 samples
INFO: ==== unexpected PTS diff = -158 samples
INFO: ==== unexpected PTS diff = -507 samples
INFO: ==== unexpected PTS diff = -116 samples
INFO: ==== unexpected PTS diff = -29 samples
INFO: ==== unexpected PTS diff = 0 samples
INFO: ==== unexpected PTS diff = 0 samples
INFO: ==== unexpected PTS diff = 0 samples

When the tablet is rotated, everything freezes, so captured packets are not sent immediately and the audio is not captured for a few milliseconds.
On the client side, this causes buffer underflow (and glitches) as expected. The only way not to cause underflow is to increase the buffering (so that when the freeze happens, there are still enough packets in the buffer to absorb it).

I've noticed that the mod+r shortcut stops working on android 11 only when the youtube player is active?

mod+r requests the foreground app to switch orientation. It may refuse (typically if it does not support the other orientation, but not only).

A bit of a dumb question, but I wanted to ask:
is it possible to patch the com.android.shell package on Android 10 OS (using a custom patch with Magisk, on rooted devices) to allow the use of RECORD_AUDIO premission with the latest scrcpy?

Processes started by root don't need any permission to capture audio. See #4127

Processes started by root don't need any permission to capture audio. See #4127

Is running root directly with scrcpy a security risk, compared to running with the patched com.androi.shell package?

Recording does not support RAW audio codec... Why?
.\scrcpy --audio-codec=raw --no-video --no-playback --record-format=mkv --record=1 -Vverbose
scrcpy v2.2 <https://github.com/Genymobile/scrcpy>
INFO: ADB device found:
INFO:     -->   (usb)           R5CW11P6N1A            device  SM_S918B
DEBUG: Device serial: R5CW11P6N1A
DEBUG: Using server (portable): C:\Users\megapro17\Desktop\Soft\scrcpy-win64-v2.2\scrcpy-server
C:\Users\megapro17\Desktop\Soft\scrcpy-win64-v2.2\scrcpy-server: 1 file pushed, 0 skipped. 15.1 MB/s (64363 bytes in 0.004s)
[server] INFO: Device: [samsung] samsung SM-S918B (Android 13)
DEBUG: Server connected
DEBUG: Starting controller thread
INFO: Recording started to matroska file: 1
DEBUG: Starting receiver thread
DEBUG: Demuxer 'audio': starting thread
ERROR: The first audio packet is not a config packet
ERROR: Recording failed to 1
DEBUG: Recorder thread ended
DEBUG: Demuxer 'audio': end of frames
ERROR: Recorder error
DEBUG: quit...
DEBUG: Receiver stopped
[server] DEBUG: Controller stopped
[server] DEBUG: Device message sender stopped
[server] DEBUG: Audio recorder stopped
[server] ERROR: Audio recording error
java.io.IOException: android.system.ErrnoException: write failed: EPIPE (Broken pipe)
        at com.genymobile.scrcpy.IO.writeFully(IO.java:33)
        at com.genymobile.scrcpy.IO.writeFully(IO.java:40)
        at com.genymobile.scrcpy.Streamer.writeDisableStream(Streamer.java:61)
        at com.genymobile.scrcpy.AudioRawRecorder.record(AudioRawRecorder.java:50)
        at com.genymobile.scrcpy.AudioRawRecorder.lambda$start$0$com-genymobile-scrcpy-AudioRawRecorder(AudioRawRecorder.java:62)
        at com.genymobile.scrcpy.AudioRawRecorder$$ExternalSyntheticLambda0.run(Unknown Source:4)
        at java.lang.Thread.run(Thread.java:1012)
Caused by: android.system.ErrnoException: write failed: EPIPE (Broken pipe)
        at libcore.io.Linux.writeBytes(Native Method)
        at libcore.io.Linux.write(Linux.java:288)
        at libcore.io.ForwardingOs.write(ForwardingOs.java:938)
        at libcore.io.BlockGuardOs.write(BlockGuardOs.java:442)
        at android.system.Os.write(Os.java:987)
        at com.genymobile.scrcpy.IO.writeFully(IO.java:25)
        ... 6 more
DEBUG: Server disconnected
DEBUG: Server terminated

Raw audio streams are not supported by mkv or mp4. It would require some lossless codec (but not implemented).
In your stack trace, you don't use the official version, right? Because if I execute with your params, I get:
$ scrcpy --audio-codec=raw --no-video --no-playback --record-format=mkv --record=1 -Vverbose
scrcpy v2.2 <https://github.com/Genymobile/scrcpy>
WARN: Recording does not support RAW audio codec

Yeah I patched an exe just for fun
Android has c2.android.flac.encoder. You just need to add another encoder and enter it everywhere?

Yes, it is possible to add flac support.
On Windows, you need to build an FFmpeg with flac decoding support: https://github.com/rom1v/scrcpy-deps/blob/231e4e819127dc55f1bf394943ba9887c1cb6224/build_ffmpeg_windows.sh#L40-L41
And in scrcpy, you add flac everywhere in addition to opus and aac. Here is the commit for aac: 4601735e51b14e85b67fdc557868f022653b4230
PR welcome :wink:


fix up the workflow

Signed-off-by: Chuck Ha<EMAIL_ADDRESS>What this PR does / why we need it:
This PR tries another attempt at getting the docker push to work.
Which issue(s) this PR fixes (optional, in fixes #<issue number>(, fixes #<issue_number>, ...) format, will close the issue(s) when PR gets merged):
Related to #67
Special notes for your reviewer:
Release note:
NONE

Looks like a lot of trial and error. I wish I could run this to help you out here.
I am good trying out the changes you have here.
Approving w/ a hold for you to look at some suggestions I made based on some doc I read.
Feel free to cancel hold to try your change.
/hold
/lgtm

/hold cancel


Feature: persistent volumes

Mount persistent volumes on jobs using GCE disk driver.

We probably need to run a job for the CSI driver for GCE persistent disks.

CSI driver support got added through: https://github.com/ChrisTerBeke/nomad-on-gcp/commit/514b0cdf36d479992ca1b1197aa4b121fe095c71