{"slug": "one-stream-three-backends-streaming-ffmpeg-to-local-azure-and-r2-with-python", "title": "One Stream, Three Backends: Streaming FFmpeg to Local, Azure, and R2 with Python", "summary": "Storix, a Python storage library, enables streaming data from producers like FFmpeg to multiple backends—local files, Azure Blob Storage, and Cloudflare R2—without provider-specific code. The library's developer, who built Storix around native Python types, demonstrates how an async generator feeding an AsyncIterator[bytes] can be written to any configured backend using a single echo() call, eliminating temporary files and SDK branching.", "body_md": "*Why Storix uses native Python streams, provider-agnostic storage sessions, and composable layers instead of provider-specific upload code.*\n\nA process is already producing data.\n\nIt might be FFmpeg generating media, a compressor writing an archive, an HTTP request delivering an upload, a database exporting records, or an inference pipeline producing artifacts.\n\nThe storage destination should not determine how that producer works.\n\nA common workflow looks like this:\n\n``` php\nproducer\n-> write a temporary file\n-> reopen the file\n-> upload it through a provider-specific SDK\n-> delete the temporary file\n```\n\nThat works, but it spreads storage concerns into the producer and creates an intermediate file that may not need to exist.\n\nI wanted the flow to look like this instead:\n\n``` php\nproducer\n-> Iterable[bytes] or AsyncIterable[bytes]\n-> Storix\n-> configured storage backend\n```\n\nIn this demo, FFmpeg generates a fragmented MP4 through stdout. Python exposes that output as an `AsyncIterator[bytes]`\n\n, and Storix writes the same stream to:\n\nThe Python code and logical destination path stay fixed. Only the selected Storix configuration changes.\n\nThe demo uses three destinations to keep the sequence short and readable. Storix also supports Azure Data Lake Gen2, Amazon S3 and compatible stores such as MinIO, and Google Cloud Storage.\n\nThis is the part of the demo that matters:\n\n``` python\nfrom storix.aio import get_storage\n\nasync with get_storage() as fs:\n    await fs.mkdir(\"/launch\", parents=True)\n    await fs.echo(\n        ffmpeg_stream(),\n        \"/launch/one-stream-three-backends.mp4\",\n        chunk_size=4 * 1024 * 1024,\n    )\n```\n\nThere is no Azure SDK, S3 SDK, or local filesystem branch in the application logic.\n\nThere is also no application-managed temporary video file.\n\nFFmpeg produces chunks. Storix consumes them.\n\nDeveloper experience is one of the main reasons I built Storix around native Python types.\n\nYou should not have to convert data into a library-specific upload object before it can be stored.\n\n`echo()`\n\naccepts the values Python developers already work with:\n\n`str`\n\n, `bytes`\n\n, `bytearray`\n\n, and other buffer-compatible objects`IO[str]`\n\nand `IO[bytes]`\n\n, including regular `open(...)`\n\nfile objects`Iterable[str | Buffer]`\n\nand `Iterable[bytes | Buffer]`\n\n`AsyncIterable[str | Buffer]`\n\nand `AsyncIterable[bytes | Buffer]`\n\nthrough `storix.aio`\n\nThe public contract is built from standard Python `IO`\n\n, `Iterable`\n\n, `AsyncIterable`\n\n, and buffer-protocol types rather than a Storix-specific stream class.\n\nThe FFmpeg producer is therefore an ordinary async generator:\n\n``` python\nimport asyncio\nimport contextlib\n\nfrom collections.abc import AsyncIterator\n\nasync def ffmpeg_stream() -> AsyncIterator[bytes]:\n    process = await asyncio.create_subprocess_exec(\n        *command,\n        stdout=asyncio.subprocess.PIPE,\n        stderr=asyncio.subprocess.PIPE,\n    )\n\n    assert process.stdout is not None\n    assert process.stderr is not None\n\n    # Drain stderr while stdout is being consumed so FFmpeg cannot block on a\n    # full error pipe. The complete sample also reports FFmpeg failures.\n    stderr_task = asyncio.create_task(process.stderr.read())\n\n    try:\n        while chunk := await process.stdout.read(64 * 1024):\n            yield chunk\n\n        return_code = await process.wait()\n        stderr = await stderr_task\n\n        if return_code != 0:\n            detail = stderr.decode(\"utf-8\", errors=\"replace\").strip()\n            raise RuntimeError(detail or f\"FFmpeg exited with status {return_code}\")\n    finally:\n        if process.returncode is None:\n            process.kill()\n            await process.wait()\n\n        if not stderr_task.done():\n            stderr_task.cancel()\n\n        with contextlib.suppress(asyncio.CancelledError):\n            await stderr_task\n```\n\nNothing in this function knows that Storix exists.\n\nIt could feed those chunks into an HTTP response, a message broker, a hashing pipeline, a parser, or any other consumer that accepts an async iterable.\n\nStorix is only the destination.\n\nSee the [ echo() reference](https://storix.mghalix.com/reference/storix/#echo) for the complete input contract.\n\nNot every workflow begins with a subprocess.\n\nA regular file opened through Python can be passed directly:\n\n``` python\nfrom storix.aio import get_storage\n\nasync with get_storage(\"local\", base=\"./data\") as fs:\n    await fs.mkdir(\"/reports\", parents=True)\n\n    with open(\"report.parquet\", \"rb\") as source:\n        await fs.echo(source, \"/reports/report.parquet\")\n```\n\nText files work the same way:\n\n```\nasync with get_storage() as fs:\n    await fs.mkdir(\"/events\", parents=True)\n\n    with open(\"events.ndjson\", encoding=\"utf-8\") as source:\n        await fs.echo(source, \"/events/events.ndjson\")\n```\n\nYou can also produce chunks yourself:\n\n``` php\nfrom collections.abc import AsyncIterator\n\nasync def generate_export() -> AsyncIterator[bytes]:\n    async for row in database_rows():\n        yield encode_row(row)\n\nasync with get_storage() as fs:\n    await fs.mkdir(\"/exports\", parents=True)\n    await fs.echo(\n        generate_export(),\n        \"/exports/customers.ndjson\",\n    )\n```\n\nThe storage API does not force the producer to become storage-aware.\n\nWriting is only half of the flow.\n\nStorix can read files incrementally with `stream()`\n\n.\n\nThe synchronous API produces a regular iterator:\n\n``` python\nfrom storix import get_storage\n\nwith get_storage(\"local\", base=\"./data\") as fs:\n    for chunk in fs.stream(\"/videos/source.mp4\"):\n        downstream.send(chunk)\n```\n\nThe asynchronous API produces an async iterator:\n\n``` python\nfrom storix.aio import get_storage\n\nasync with get_storage() as fs:\n    async for chunk in fs.stream(\n        \"/videos/source.mp4\",\n        chunk_size=64 * 1024,\n    ):\n        await downstream.send(chunk)\n```\n\nThe downstream consumer might be an HTTP response, decompressor, parser, media processor, hashing pipeline, inference component, or another storage destination.\n\nFor small, known-size files, `cat()`\n\nreturns the complete contents as `bytes`\n\n. For larger workloads, `stream()`\n\nlets the application process data incrementally instead of materializing the complete object first.\n\nFor downloads into a seekable file, Storix 0.5.0 can go further than an ordered stream. `download()`\n\nmay fetch several byte ranges of one large object concurrently and write each range at its destination offset. `stream()`\n\nremains the ordered incremental API for arbitrary consumers.\n\nIn one measured 200 MiB Azure download over a home connection, eight ranges reduced wall time from 61.53 seconds to 25.51 seconds, with peak RSS of 173 MB. This is one measurement, not a universal speed guarantee. Each range is a separate request, so the throughput improvement trades against transaction count. Use `ranges=1`\n\n, or `STORIX_MAX_TRANSFER_RANGES=1`\n\n, to keep every download on one stream.\n\nSee:\n\nThe demo uses `get_storage()`\n\nwithout naming a provider in the Python code:\n\n``` python\nfrom storix.aio import get_storage\n\nasync with get_storage() as fs:\n    ...\n```\n\nThe active provider can come from environment configuration:\n\n```\nSTORIX_PROVIDER=local uv run python demo.py\nSTORIX_PROVIDER=azure uv run python demo.py\nSTORIX_PROVIDER=s3 uv run python demo.py\n```\n\nProvider-specific settings remain namespaced.\n\nFor Azure:\n\n```\nSTORIX_PROVIDER=azure\nSTORIX_AZURE_CONTAINER=storix-demo\nSTORIX_AZURE_ACCOUNT_NAME=my-account\nSTORIX_AZURE_CREDENTIAL=...\n\n# Optional. The default is auto.\nSTORIX_AZURE_KIND=blob\n```\n\nWith `kind=\"auto\"`\n\n, Storix checks whether the account has hierarchical namespaces enabled and selects either the ADLS Gen2 backend or the Blob backend. Explicit `blob`\n\nor `adls`\n\nselection skips that detection when the intended surface is already known or account-level detection is unavailable.\n\nFor Cloudflare R2:\n\n```\nSTORIX_PROVIDER=s3\nSTORIX_S3_BUCKET=storix-demo\nSTORIX_S3_REGION=auto\nSTORIX_S3_ENDPOINT=https://<account-id>.r2.cloudflarestorage.com\nSTORIX_S3_ACCESS_KEY_ID=...\nSTORIX_S3_SECRET_ACCESS_KEY=...\n```\n\nR2 uses Storix's S3 backend because it exposes an S3-compatible API. Cloudflare's SDK guidance uses `region_name=\"auto\"`\n\n; the value is required by AWS SDK conventions but is not used as an R2 region.\n\nSee:\n\nStorix 0.5.0 gives that composition boundary a name.\n\nA *profile* is one provider plus its settings. A *stage* overlays what differs between deployments:\n\n```\n# storix.toml, or ~/.config/storix/config.toml\n[profiles.ingest]\nprovider = \"azure\"\ncontainer = \"raw\"\ndefault_environment = \"dev\"\n\n[profiles.ingest.environments.dev]\naccount_name = \"acmedevstorage\"\ncredential = \"env:ACME_DEV_CREDENTIAL\"\n\n[profiles.ingest.environments.prod]\naccount_name = \"acmeprdstorage\"\ncredential = \"env:ACME_PRD_CREDENTIAL\"\n\n[profiles.archive]\nprovider = \"s3\"\nbucket = \"archive\"\nregion = \"auto\"\nendpoint = \"https://<account-id>.r2.cloudflarestorage.com\"\nfs = get_storage(\n    profile=\"ingest\",\n    environment=os.environ[\"STAGE\"],\n)\nsx --profile ingest --env prod ls /\n```\n\nNon-secret coordinates can live in the file. A credential can be named without being stored there:\n\n```\ncredential = \"env:ACME_PRD_CREDENTIAL\"\n```\n\nEach stage can name its own credential variable, so a deployment only needs to expose the credential for the stage it runs. An unset variable fails during configuration loading instead of falling back to another stage.\n\nA pinned profile and `STORIX_PROFILE`\n\nsteer `sx`\n\n, deliberately not a plain `get_storage()`\n\ncall. The library selects a profile only when the call asks for one, so personal CLI configuration cannot silently redirect application code.\n\n```\nsrc = get_storage(\"azure\", container=\"raw\")\ndst = get_storage(\"s3\", bucket=\"archive\")\n```\n\nProfiles sit alongside direct environment and explicit configuration rather than replacing them. `sx config show --effective`\n\nreports the configuration source that supplied each value.\n\nEnvironment-driven selection is useful when an application has one active provider, but some systems need several storage sessions at the same time.\n\nEvery provider can be configured explicitly:\n\n``` python\nfrom storix.aio import get_storage\n\nraw = get_storage(\n    \"azure\",\n    container=\"raw\",\n    account_name=settings.azure_account_name,\n    credential=settings.azure_credential,\n)\n\nprocessed = get_storage(\n    \"s3\",\n    bucket=settings.processed_bucket,\n    region=settings.s3_region,\n    endpoint=settings.s3_endpoint,\n)\n```\n\nThose sessions can be injected into a pipeline:\n\n``` python\nfrom storix.aio import Storix\n\nclass MaterializationPipeline:\n    def __init__(\n        self,\n        *,\n        raw: Storix,\n        staging: Storix,\n        processed: Storix,\n    ) -> None:\n        self.raw = raw\n        self.staging = staging\n        self.processed = processed\n```\n\nConfiguration remains provider-specific where it needs to be. The filesystem operations performed by the application remain consistent.\n\nThe demo writes to one logical path:\n\n```\n/launch/one-stream-three-backends.mp4\n```\n\n`fs.locate()`\n\nreveals the physical URI selected by the backend:\n\n``` php\nLocal       -> file:///.../launch/one-stream-three-backends.mp4\nAzure Blob  -> wasbs://storix-demo@my-account.blob.core.windows.net/launch/one-stream-three-backends.mp4\nAzure ADLS  -> abfss://storix-demo@my-account.dfs.core.windows.net/launch/one-stream-three-backends.mp4\nR2 / S3     -> s3://storix-demo/launch/one-stream-three-backends.mp4\n```\n\nApplication code works with one Unix-style filesystem model. Storix handles the provider boundary underneath it.\n\n`echo()`\n\nand `stream()`\n\nStorix is not only an upload helper, and it is more than a cloud-aware path object.\n\nA session provides a broader filesystem API across its backends:\n\n```\nawait fs.mkdir(\"/datasets/processed\", parents=True)\n\npaths = await fs.ls(\"/datasets\", abs=True)\n\nasync for entry in fs.walk(\"/datasets/raw\"):\n    print(entry.path, entry.kind, entry.size)\n\nasync for entry in fs.find(\n    \"/datasets\",\n    name=\"*.parquet\",\n    kind=\"file\",\n):\n    print(entry.path)\n\nmatches = [\n    path\n    async for path in fs.glob(\n        \"**/*.json\",\n        \"/datasets\",\n    )\n]\n\nawait fs.cp(\n    \"/datasets/staging/batch-42\",\n    \"/datasets/processed\",\n    recursive=True,\n)\n\nsize = await fs.du(\"/datasets/processed\")\nproperties = await fs.stat(\"/datasets/processed/result.parquet\")\n\nawait fs.rm(\n    \"/datasets/staging/batch-42\",\n    recursive=True,\n)\n```\n\nThe API includes familiar controls for listing, lazy scanning, recursive walking, searching, globbing, copying, moving, removal, metadata, apparent size, current directories, sandboxes, temporary workspaces, and provider-native URLs where supported.\n\nSee the complete [Storix session reference](https://storix.mghalix.com/reference/storix/).\n\nProviders still have different native capabilities. Storix reports those capabilities explicitly, while layers can backfill selected behavior when a meaningful portable implementation exists.\n\nSome storage behavior should remain stable even when the provider changes.\n\nA cloud backend can often produce a provider-native URL. A local filesystem cannot. For small local UI assets, `DataUrlLayer`\n\ncan backfill the missing capability:\n\n``` python\nfrom storix.aio import DataUrlLayer, get_storage\n\nfs = get_storage().with_layer_missing(DataUrlLayer)\nresult_url = await fs.url(\"/results/detected-person.jpg\")\n```\n\n`with_layer_missing()`\n\nprefers a native implementation and adds the layer only when the capability is absent:\n\n``` php\nbackend with native URL support\n-> use the backend implementation\n\nbackend without URL support\n-> add DataUrlLayer\n-> return an inline data: URL\n```\n\nUsing `with_layer(DataUrlLayer)`\n\napplies it unconditionally. `fs.data_url(path)`\n\nis also available when the caller explicitly wants an inline representation.\n\nData URLs are useful for small browser-rendered assets, but they are unsuitable for large media and expensive inside an LLM context. A different application can provide a custom URL layer backed by its own media gateway while continuing to call `fs.url(path)`\n\n.\n\n`MetadataLayer`\n\nsolves the same portability problem for custom metadata:\n\n``` python\nfrom storix.aio import MetadataLayer, get_storage\n\nfs = get_storage().with_layer_missing(MetadataLayer)\n```\n\nWhen the backend supports custom metadata natively, the layer is skipped. Otherwise it preserves metadata through a hidden sidecar stored with the data.\n\nSerialization is customizable:\n\n```\nfs = get_storage().with_layer_missing(\n    MetadataLayer,\n    serialize=serializer.dumpb,\n    deserialize=serializer.loads,\n)\n```\n\nStorix's local backend does not expose native object metadata, while cloud object stores can. The pipeline still reads the same metadata through Storix regardless of where a sample is stored.\n\nSee [Layers](https://storix.mghalix.com/guide/layers/) for capability-aware composition and the built-in layer stack.\n\n`CacheLayer`\n\nwraps the same storage port, so one cache policy can operate over local storage, Azure, S3, GCS, or a custom backend.\n\nIts cache store is replaceable too. Storix ships an in-memory store, while the `CacheStore`\n\nprotocol requires only:\n\n```\nget(key, default=None)\nset(key, value, *, expire=None)\ndelete(key)\ndelete_match(pattern)\n```\n\nFor async Storix, a Cashews cache already satisfies that protocol and can use memory, disk, local Redis, or managed Redis.\n\nMetadata, directory sizes, URLs, and file contents can each have their own TTL, store, and limits:\n\n``` python\nfrom storix.aio import CacheLayer, cache, get_storage\n\nfs = get_storage(\"azure\").with_layer(\n    CacheLayer,\n    store=stores[settings.default_store],\n    ttl=settings.default_ttl,\n    environment=settings.environment,\n    metadata=cache(ttl=settings.metadata_ttl),\n    du=cache(ttl=settings.du_ttl),\n    url=cache(ttl=settings.url_ttl),\n    read=cache(\n        ttl=settings.read_ttl,\n        max_bytes=settings.read_max_bytes,\n        store=stores[settings.read_store],\n    ),\n)\n```\n\nThe same structural design supports custom backends and custom layers. A backend implements `StorageBackend`\n\n; a layer wraps that same port and overrides only the operations it needs. Existing sessions, the CLI, and other layers continue to work without provider-specific rewrites.\n\nExamples include audit events, content validation, encryption, tracing, notifications, organization-specific authorization, and domain-specific metadata.\n\nSee:\n\nThe Python API and `sx`\n\nCLI drive the core. The `StorageBackend`\n\nport isolates storage implementations. Layers implement that same port and wrap any backend. `CacheLayer`\n\nintroduces another small port for replaceable cache stores.\n\n```\nPython API                 sx CLI\n    \\                       /\n             Storix core\n      cwd / home / path resolution\n             Unix operations\n                    |\n         StorageBackend protocol\n                    |\n        +-----------+-----------+\n        | composable layers     |\n        | sandbox / cache       |\n        | URL / metadata        |\n        | observability / custom|\n        +-----------+-----------+\n                    |\n         StorageBackend protocol\n                    |\n   +----------------+----------------+\n   | memory | local | Azure | S3/R2 |\n   | MinIO  | GCS   | custom backends|\n   +---------------------------------+\n\nCacheLayer\n    -> CacheStore protocol\n    -> in-memory / Redis / disk / custom adapter\n```\n\nThis separation lets Storix add providers, middleware, cache technologies, and user-facing interfaces without moving every concern into one monolithic abstraction.\n\nStorix was shaped by workflows that had to survive the path from isolated tests to cloud production.\n\nTests can use a disposable in-process backend:\n\n``` python\nfrom storix.aio import get_storage\n\nfs = get_storage(\"memory\")\n```\n\nLocal prototypes can write inspectable files:\n\n```\nfs = get_storage(\"local\", base=\"./development-data\")\n```\n\nLater, deployment configuration can select Azure, S3, GCS, or an S3-compatible service without rewriting the shared pipeline operations.\n\nComputer vision systems handle source images and videos, extracted frames, generated clips, inference artifacts, reference media, and domain metadata.\n\nThe inference pipeline should process those assets and maintain their metadata. It should not contain separate storage branches for local development and cloud deployment. The `MetadataLayer`\n\nand `DataUrlLayer`\n\nexamples above came directly from this need.\n\nI have used the same streaming pattern to materialize more than a terabyte of YouTube videos directly into the selected storage provider.\n\n``` php\nvideo producer\n-> chunk stream\n-> Storix\n-> selected provider\n```\n\nThe workflow does not first load a complete video into application memory and does not require a second provider-specific upload pass.\n\nThe same model runs in scheduled audio-library synchronization and highly concurrent data-engineering workloads across raw, staging, and processed zones.\n\nEach storage zone can have its own container, bucket, prefix, provider, credentials, cache policy, sandbox, and observability hooks. The processing components retain the same filesystem operations.\n\nLow-resource and ephemeral environments benefit too. Removing unnecessary materialization reduces RAM and temporary-disk pressure and avoids making correctness depend on the lifetime of one application instance.\n\nStorix also ships `sx`\n\n, a Unix-flavored CLI over the same sessions, providers, layers, and typed errors.\n\nInstall it on POSIX systems:\n\n```\ncurl -LsSf https://storix.mghalix.com/install.sh | sh\n```\n\nOr on Windows:\n\n```\npowershell -c \"irm https://storix.mghalix.com/install.ps1 | iex\"\n```\n\nBoth installers are thin wrappers over `uv tool install`\n\n. They accept provider selections such as `--with azure,s3`\n\n, `--all`\n\n, and `--version`\n\n, require no root access, ask for no credentials, write no configuration, and do not edit shell startup files.\n\nYou can also use uv directly:\n\n```\nuv tool install \"storix[cli,azure,s3]>=0.5.0,<0.6.0\"\nuv tool install \"storix[all]>=0.5.0,<0.6.0\"\n```\n\n`sx update`\n\nupgrades installations owned by `uv tool`\n\n, preserving the extras recorded in uv's receipt. In editable, virtual-environment, and other installation modes, it refuses to rewrite the environment and prints manual upgrade guidance.\n\nRun one command:\n\n```\nsx -p azure tree --long --level 2\nsx -p azure du -sh /knowledge-base\n```\n\nOr enter an interactive session:\n\n```\nsx -p azure\n```\n\nThe shell retains its current directory and tab-completes commands and paths. `push`\n\nand `pull`\n\ncomplete local or remote paths according to the argument position and stream files or complete directory trees with progress reporting:\n\n```\nsx push ./media /knowledge-base/media\nsx pull /knowledge-base/results ./results\n```\n\nMissing destination parents are created automatically inside the configured storage root. The bucket or container itself remains a provider control-plane resource and must already exist, except where `sx provision`\n\nexplicitly supports the backend.\n\nWhen a session is not where you expected, `whereami`\n\nshows the connection without making you guess:\n\n``` bash\n$ sx --profile ingest --env prod whereami\nbackend:  AzureBackend\nprofile:  ingest (stage: prod)\nroot uri: abfss://raw@acmeprdstorage.dfs.core.windows.net/\ncwd:      /\nhome:     /\nlayers:   cache ls/stat/du/cat via InMemoryCacheStore\n```\n\n`sx config show --effective`\n\nprints each effective field with the configuration source that supplied it: a flag, stage overlay, profile, process environment, `.env`\n\n, project file, user file, or built-in default. `sx config sources`\n\nlists discovered files and precedence. `sx doctor`\n\nreports the installation method, importable provider extras, selected profile and stage, and configuration discovery without opening a connection or resolving a credential.\n\nProject or personal configuration can provide provider coordinates, CLI preferences, layers, and aliases:\n\n```\n# storix.toml\nprovider = \"azure\"\n\n[azure]\naccount_name = \"example\"\ncontainer = \"media\"\ncredential = \"env:AZURE_STORAGE_CREDENTIAL\"\n\n[cli]\nicons = true\nlayers = [\n    { name = \"cache\", ttl = 300 },\n]\n\n[cli.alias]\nl = \"ls -l\"\nll = \"ls -la\"\nlt = \"tree --level 2\"\nlT = \"tree --long\"\n```\n\nThe in-memory cache lives for the duration of one `sx`\n\nprocess, so it is most useful while repeatedly navigating an interactive session:\n\n``` bash\n$ sx -p azure\nstorix shell\nconnected to AzureBlobBackend\ncache ls/stat/du/cat via InMemoryCacheStore - type refresh to clear\n\n/ > cd /knowledge-base\n/knowledge-base > lT\n/knowledge-base > lT\n```\n\nThe first traversal reaches remote storage. Repeated reads can reuse cached values until the TTL expires or `refresh`\n\nclears them.\n\n`sx`\n\nalso includes provider flags such as `--base`\n\n, `--bucket`\n\n, `--container`\n\n, `--account-name`\n\n, `--region`\n\n, `--endpoint`\n\n, `--root`\n\n, and `--kind`\n\n, plus a typed `--set provider.field=value`\n\nescape hatch for less common non-secret coordinates.\n\nSee [The sx CLI](https://storix.mghalix.com/guide/cli/).\n\nThis demo demonstrates architectural portability. It is not a provider benchmark.\n\nThe elapsed times shown in the video include different networks, services, account configurations, and initialization paths. They should not be interpreted as a direct performance comparison.\n\nProvider credentials and deployment settings remain provider-specific. Providers also expose different native capabilities. Storix keeps those differences at the composition boundary and makes capabilities explicit.\n\nWhat the demo proves is focused:\n\nA producer can expose ordinary Python chunks, and the same application code can stream those chunks into multiple storage systems.\n\nStorix is pre-1.0 and follows a documented versioning convention:\n\nTo stay on the 0.5 release line:\n\n```\nuv add \"storix[azure,s3]>=0.5.0,<0.6.0\"\n```\n\nThe video was recorded with Storix 0.4.6. The streaming write shown in it remains valid in 0.5.0. This article and its installation instructions target 0.5.0, which adds unified provider configuration, profiles and stages, standalone `sx`\n\ninstallation, CLI diagnostics, and parallel range downloads.\n\nSee the [versioning policy](https://github.com/mghalix/storix/blob/main/docs/adr/0021-versioning-policy.md).\n\nFor a zero-configuration experiment, start with memory:\n\n``` php\nimport asyncio\n\nfrom storix.aio import get_storage\n\nasync def main() -> None:\n    async with get_storage(\"memory\") as fs:\n        await fs.mkdir(\"/reports\")\n        await fs.echo(\n            b\"quarterly numbers\",\n            \"/reports/q1.txt\",\n        )\n\n        print(await fs.cat(\"/reports/q1.txt\"))\n        print(await fs.ls(\"/reports\"))\n\nasyncio.run(main())\n```\n\nThen switch to local storage:\n\n```\nfs = get_storage(\n    \"local\",\n    base=\"./storix-data\",\n)\n```\n\nOr select a cloud provider through configuration without rewriting the operations around it.\n\nStorix is still pre-1.0, and real workflows are the most valuable input into its direction.\n\nI am especially interested in cases where:\n\nShare the producer, destination, approximate data volume, and what feels awkward today in the [Storix Discussions](https://github.com/mghalix/storix/discussions).\n\nThe goal is not to invent abstractions in isolation.\n\nIt is to make real storage workflows feel like Python.\n\n```\nOne stream.\nThree backends.\nZero storage rewrites.\n```\n\n`sx`\n\nCLI", "url": "https://wpnews.pro/news/one-stream-three-backends-streaming-ffmpeg-to-local-azure-and-r2-with-python", "canonical_source": "https://dev.to/mghalix/one-stream-three-backends-streaming-ffmpeg-to-local-azure-and-r2-with-python-2iii", "published_at": "2026-08-03 14:04:53+00:00", "updated_at": "2026-08-03 14:14:18.119751+00:00", "lang": "en", "topics": ["developer-tools", "ai-infrastructure"], "entities": ["Storix", "FFmpeg", "Azure", "Cloudflare R2", "Python"], "alternates": {"html": "https://wpnews.pro/news/one-stream-three-backends-streaming-ffmpeg-to-local-azure-and-r2-with-python", "markdown": "https://wpnews.pro/news/one-stream-three-backends-streaming-ffmpeg-to-local-azure-and-r2-with-python.md", "text": "https://wpnews.pro/news/one-stream-three-backends-streaming-ffmpeg-to-local-azure-and-r2-with-python.txt", "jsonld": "https://wpnews.pro/news/one-stream-three-backends-streaming-ffmpeg-to-local-azure-and-r2-with-python.jsonld"}}